Skip to content

Commit

Permalink
Client/Server decoder with typing.
Browse files Browse the repository at this point in the history
  • Loading branch information
janiversen committed Oct 16, 2024
1 parent 80a73d2 commit 782cf16
Show file tree
Hide file tree
Showing 18 changed files with 52 additions and 93 deletions.
3 changes: 1 addition & 2 deletions doc/source/roadmap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ The following bullet points are what the maintainers focus on:
- more typing in the core
- 100% test coverage fixed for all new parts (currently transport and framer)
- Updated PDU, moving client/server decoder into pdu
- better broadcast handling
- better broadcast handling
- better client no_response handling
- Simplify PDU classes
- better retry handling (only disconnect when really needed)
- 3.7.5, bug fix release, hopefully with:
Expand Down
2 changes: 1 addition & 1 deletion examples/client_custom_msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# Since the function code is already registered with the decoder factory,
# this will be decoded as a read coil response. If you implement a new
# method that is not currently implemented, you must register the request
# and response with a ClientDecoder factory.
# and response with a DecoderResponses factory.
# --------------------------------------------------------------------------- #


Expand Down
6 changes: 3 additions & 3 deletions examples/message_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
FramerRTU,
FramerSocket,
)
from pymodbus.pdu import ClientDecoder, ServerDecoder
from pymodbus.pdu import DecoderRequests, DecoderResponses


_logger = logging.getLogger(__file__)
Expand Down Expand Up @@ -73,8 +73,8 @@ def decode(self, message):
print(f"Decoding Message {value}")
print("=" * 80)
decoders = [
self.framer(ServerDecoder()),
self.framer(ClientDecoder()),
self.framer(DecoderRequests()),
self.framer(DecoderResponses()),
]
for decoder in decoders:
print(f"{decoder.decoder.__class__.__name__}")
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pymodbus.exceptions import ConnectionException, ModbusIOException
from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerBase, FramerType
from pymodbus.logging import Log
from pymodbus.pdu import ClientDecoder, ModbusPDU
from pymodbus.pdu import DecoderResponses, ModbusPDU
from pymodbus.transaction import SyncModbusTransactionManager
from pymodbus.transport import CommParams
from pymodbus.utilities import ModbusTransactionState
Expand Down Expand Up @@ -182,7 +182,7 @@ def __init__(
self.slaves: list[int] = []

# Common variables.
self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(ClientDecoder())
self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(DecoderResponses())
self.transaction = SyncModbusTransactionManager(
self,
self.retries,
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/client/modbusclientprotocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
FramerType,
)
from pymodbus.logging import Log
from pymodbus.pdu import ClientDecoder
from pymodbus.pdu import DecoderResponses
from pymodbus.transaction import ModbusTransactionManager
from pymodbus.transport import CommParams, ModbusProtocol

Expand All @@ -35,7 +35,7 @@ def __init__(
self.on_connect_callback = on_connect_callback

# Common variables.
self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(ClientDecoder())
self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(DecoderResponses())
self.transaction = ModbusTransactionManager()

def _handle_response(self, reply):
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/framer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from pymodbus.exceptions import ModbusIOException
from pymodbus.logging import Log
from pymodbus.pdu import ClientDecoder, ModbusPDU, ServerDecoder
from pymodbus.pdu import DecoderRequests, DecoderResponses, ModbusPDU


class FramerType(str, Enum):
Expand All @@ -30,7 +30,7 @@ class FramerBase:

def __init__(
self,
decoder: ClientDecoder | ServerDecoder,
decoder: DecoderResponses | DecoderRequests,
) -> None:
"""Initialize a ADU (framer) instance."""
self.decoder = decoder
Expand Down
6 changes: 3 additions & 3 deletions pymodbus/pdu/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Framer."""
__all__ = [
"ClientDecoder",
"DecoderRequests",
"DecoderResponses",
"ExceptionResponse",
"ModbusExceptions",
"ModbusPDU",
"ServerDecoder"
]

from pymodbus.pdu.decoders import ClientDecoder, ServerDecoder
from pymodbus.pdu.decoders import DecoderRequests, DecoderResponses
from pymodbus.pdu.pdu import (
ExceptionResponse,
ModbusExceptions,
Expand Down
60 changes: 10 additions & 50 deletions pymodbus/pdu/decoders.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
"""Modbus Request/Response Decoder Factories.
The following factories make it easy to decode request/response messages.
To add a new request/response pair to be decodeable by the library, simply
add them to the respective function lookup table (order doesn't matter, but
it does help keep things organized).
Regardless of how many functions are added to the lookup, O(1) behavior is
kept as a result of a pre-computed lookup dictionary.
"""

# pylint: disable=missing-type-doc
"""Modbus Request/Response Decoders."""
from collections.abc import Callable

import pymodbus.pdu.bit_read_message as bit_r_msg
Expand All @@ -28,11 +17,8 @@
# --------------------------------------------------------------------------- #
# Server Decoder
# --------------------------------------------------------------------------- #
class ServerDecoder:
"""Request Message Factory (Server).
To add more implemented functions, simply add them to the list
"""
class DecoderRequests:
"""Decode request Message (Server)."""

__function_table = [
reg_r_msg.ReadHoldingRegistersRequest,
Expand Down Expand Up @@ -90,33 +76,19 @@ def __init__(self) -> None:
self.__sub_lookup[f.function_code][f.sub_function_code] = f # type: ignore[attr-defined]

def decode(self, message):
"""Decode a request packet.
:param message: The raw modbus request packet
:return: The decoded modbus message or None if error
"""
"""Decode a request packet."""
try:
return self._helper(message)
except ModbusException as exc:
Log.warning("Unable to decode request {}", exc)
return None

def lookupPduClass(self, function_code):
"""Use `function_code` to determine the class of the PDU.
:param function_code: The function code specified in a frame.
:returns: The class of the PDU that has a matching `function_code`.
"""
"""Use `function_code` to determine the class of the PDU."""
return self.lookup.get(function_code, base.ExceptionResponse)

def _helper(self, data: str):
"""Generate the correct request object from a valid request packet.
This decodes from a list of the currently implemented request types.
:param data: The request packet to decode
:returns: The decoded request or illegal function request object
"""
"""Generate the correct request object from a valid request packet."""
function_code = int(data[0])
if not (request := self.lookup.get(function_code, lambda: None)()):
Log.debug("Factory Request[{}]", function_code)
Expand Down Expand Up @@ -144,11 +116,7 @@ def _helper(self, data: str):
return request

def register(self, function):
"""Register a function and sub function class with the decoder.
:param function: Custom function class to register
:raises MessageRegisterException:
"""
"""Register a function and sub function class with the decoder."""
if not issubclass(function, base.ModbusPDU):
raise MessageRegisterException(
f'"{function.__class__.__name__}" is Not a valid Modbus Message'
Expand All @@ -167,7 +135,7 @@ def register(self, function):
# --------------------------------------------------------------------------- #
# Client Decoder
# --------------------------------------------------------------------------- #
class ClientDecoder:
class DecoderResponses:
"""Response Message Factory (Client).
To add more implemented functions, simply add them to the list
Expand Down Expand Up @@ -224,19 +192,11 @@ def __init__(self) -> None:
self.__sub_lookup[f.function_code][f.sub_function_code] = f # type: ignore[attr-defined]

def lookupPduClass(self, function_code):
"""Use `function_code` to determine the class of the PDU.
:param function_code: The function code specified in a frame.
:returns: The class of the PDU that has a matching `function_code`.
"""
"""Use `function_code` to determine the class of the PDU."""
return self.lookup.get(function_code, base.ExceptionResponse)

def decode(self, message):
"""Decode a response packet.
:param message: The raw packet to decode
:return: The decoded modbus message or None if error
"""
"""Decode a response packet."""
try:
return self._helper(message)
except ModbusException as exc:
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/server/async_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from pymodbus.exceptions import NoSuchSlaveException
from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerBase, FramerType
from pymodbus.logging import Log
from pymodbus.pdu import DecoderRequests
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.pdu import ServerDecoder
from pymodbus.transport import CommParams, CommType, ModbusProtocol


Expand Down Expand Up @@ -257,7 +257,7 @@ def __init__(
True,
)
self.loop = asyncio.get_running_loop()
self.decoder = ServerDecoder()
self.decoder = DecoderRequests()
self.context = context or ModbusServerContext()
self.control = ModbusControlBlock()
self.ignore_missing_slaves = ignore_missing_slaves
Expand Down
4 changes: 2 additions & 2 deletions pymodbus/server/simulator/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from pymodbus.datastore.simulator import Label
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.logging import Log
from pymodbus.pdu import ExceptionResponse, ServerDecoder
from pymodbus.pdu import DecoderRequests, ExceptionResponse
from pymodbus.server.async_io import (
ModbusSerialServer,
ModbusTcpServer,
Expand Down Expand Up @@ -214,7 +214,7 @@ def __init__(
self.refresh_rate = 0
self.register_filter: list[int] = []
self.call_list: list[CallTracer] = []
self.request_lookup = ServerDecoder.getFCdict()
self.request_lookup = DecoderRequests.getFCdict()
self.call_monitor = CallTypeMonitor()
self.call_response = CallTypeResponse()
app_key = getattr(web, 'AppKey', str) # fall back to str for aiohttp < 3.9.0
Expand Down
4 changes: 2 additions & 2 deletions test/framer/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType
from pymodbus.pdu import ClientDecoder, ServerDecoder
from pymodbus.pdu import DecoderRequests, DecoderResponses


@pytest.fixture(name="entry")
Expand All @@ -21,5 +21,5 @@ def prepare_is_server():
async def prepare_test_framer(entry, is_server):
"""Return framer object."""
return FRAMER_NAME_TO_CLASS[entry](
(ServerDecoder if is_server else ClientDecoder)(),
(DecoderRequests if is_server else DecoderResponses)(),
)
6 changes: 3 additions & 3 deletions test/framer/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
FramerSocket,
FramerTLS,
)
from pymodbus.pdu import ClientDecoder, ServerDecoder
from pymodbus.pdu import DecoderRequests, DecoderResponses
from pymodbus.pdu import ModbusExceptions as merror
from pymodbus.pdu.register_read_message import (
ReadHoldingRegistersRequest,
Expand All @@ -23,13 +23,13 @@ def set_calls():
print(f" dev_id --> {dev_id}")
for tid in (0, 3077):
print(f" tid --> {tid}")
client = framer(ClientDecoder())
client = framer(DecoderResponses())
request = ReadHoldingRegistersRequest(124, 2, dev_id)
request.transaction_id = tid
result = client.buildFrame(request)
print(f" request --> {result}")
print(f" request --> {result.hex()}")
server = framer(ServerDecoder())
server = framer(DecoderRequests())
response = ReadHoldingRegistersResponse([141,142])
response.slave_id = dev_id
response.transaction_id = tid
Expand Down
4 changes: 2 additions & 2 deletions test/framer/test_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
FramerSocket,
FramerTLS,
)
from pymodbus.pdu import ServerDecoder
from pymodbus.pdu import DecoderRequests


TEST_MESSAGE = b"\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d"
Expand All @@ -30,7 +30,7 @@ class TestExtas:
def setup_method(self):
"""Set up the test environment."""
self.client = None
self.decoder = ServerDecoder()
self.decoder = DecoderRequests()
self._tcp = FramerSocket(self.decoder)
self._tls = FramerTLS(self.decoder)
self._rtu = FramerRTU(self.decoder)
Expand Down
6 changes: 3 additions & 3 deletions test/framer/test_framer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
FramerTLS,
FramerType,
)
from pymodbus.pdu import ClientDecoder, ModbusPDU
from pymodbus.pdu import DecoderResponses, ModbusPDU

from .generator import set_calls

Expand All @@ -27,7 +27,7 @@ def test_setup(self, entry, is_server):

def test_base(self):
"""Test FramerBase."""
framer = FramerBase(ClientDecoder())
framer = FramerBase(DecoderResponses())
framer.decode(b'')
framer.encode(b'', 0, 0)
framer.encode(b'', 2, 0)
Expand Down Expand Up @@ -189,7 +189,7 @@ def test_encode_type(self, frame, frame_expected, data, dev_id, tr_id, inx1, inx
"""Test encode method."""
if frame == FramerTLS and dev_id + tr_id:
return
frame_obj = frame(ClientDecoder())
frame_obj = frame(DecoderResponses())
expected = frame_expected[inx1 + inx2 + inx3]
encoded_data = frame_obj.encode(data, dev_id, tr_id)
assert encoded_data == expected
Expand Down
8 changes: 4 additions & 4 deletions test/framer/test_multidrop.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from pymodbus.exceptions import ModbusIOException
from pymodbus.framer import FramerAscii, FramerRTU
from pymodbus.pdu import ClientDecoder, ServerDecoder
from pymodbus.pdu import DecoderRequests, DecoderResponses


class TestMultidrop:
Expand All @@ -16,7 +16,7 @@ class TestMultidrop:
@pytest.fixture(name="framer")
def fixture_framer(self):
"""Prepare framer."""
return FramerRTU(ServerDecoder())
return FramerRTU(DecoderRequests())

@pytest.fixture(name="callback")
def fixture_callback(self):
Expand Down Expand Up @@ -50,7 +50,7 @@ def test_big_split_response_frame_from_other_id(self, framer):
"""Test split response."""
# This is a single *response* from device id 1 after being queried for 125 holding register values
# Because the response is so long it spans several serial events
framer = FramerRTU(ClientDecoder())
framer = FramerRTU(DecoderResponses())
serial_events = [
b'\x01\x03\xfa\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00',
b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00',
Expand Down Expand Up @@ -153,7 +153,7 @@ def return_none(_data):
"""Return none."""
return None

framer = FramerAscii(ServerDecoder())
framer = FramerAscii(DecoderRequests())
framer.decoder.decode = return_none
with pytest.raises(ModbusIOException):
framer.processIncomingFrame(b':1103007C00026E\r\n')
Expand Down
Loading

0 comments on commit 782cf16

Please sign in to comment.