Skip to content

Commit ed09e2f

Browse files
committed
Simplify pdu lookup.
1 parent 3029691 commit ed09e2f

File tree

8 files changed

+99
-86
lines changed

8 files changed

+99
-86
lines changed

pymodbus/pdu/bit_message.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pymodbus.constants import ExcCodes, ModbusStatus
77
from pymodbus.datastore import ModbusDeviceContext
88

9+
from .decoders import DecodePDU
910
from .exceptionresponse import ExceptionResponse
1011
from .pdu import ModbusPDU, pack_bitstring, unpack_bitstring
1112

@@ -163,3 +164,8 @@ def encode(self) -> bytes:
163164
def decode(self, data: bytes) -> None:
164165
"""Decode a write coils response."""
165166
self.address, self.count = struct.unpack(">HH", data[:4])
167+
168+
DecodePDU.add_pdu(ReadCoilsRequest, ReadCoilsResponse)
169+
DecodePDU.add_pdu(ReadDiscreteInputsRequest, ReadDiscreteInputsResponse)
170+
DecodePDU.add_pdu(WriteSingleCoilRequest, WriteSingleCoilResponse)
171+
DecodePDU.add_pdu(WriteMultipleCoilsRequest, WriteMultipleCoilsResponse)

pymodbus/pdu/decoders.py

Lines changed: 43 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,86 @@
11
"""Modbus Request/Response Decoders."""
22
from __future__ import annotations
33

4-
import pymodbus.pdu.bit_message as bit_msg
5-
import pymodbus.pdu.diag_message as diag_msg
6-
import pymodbus.pdu.file_message as file_msg
7-
import pymodbus.pdu.mei_message as mei_msg
8-
import pymodbus.pdu.other_message as o_msg
9-
import pymodbus.pdu.pdu as base
10-
import pymodbus.pdu.register_message as reg_msg
114
from pymodbus.exceptions import MessageRegisterException, ModbusException
125
from pymodbus.logging import Log
136

7+
from .pdu import ExceptionResponse, ModbusPDU
8+
149

1510
class DecodePDU:
1611
"""Decode pdu requests/responses (server/client)."""
1712

18-
_pdu_class_table: set[tuple[type[base.ModbusPDU], type[base.ModbusPDU]]] = {
19-
(reg_msg.ReadHoldingRegistersRequest, reg_msg.ReadHoldingRegistersResponse),
20-
(bit_msg.ReadDiscreteInputsRequest, bit_msg.ReadDiscreteInputsResponse),
21-
(reg_msg.ReadInputRegistersRequest, reg_msg.ReadInputRegistersResponse),
22-
(bit_msg.ReadCoilsRequest, bit_msg.ReadCoilsResponse),
23-
(bit_msg.WriteMultipleCoilsRequest, bit_msg.WriteMultipleCoilsResponse),
24-
(reg_msg.WriteMultipleRegistersRequest, reg_msg.WriteMultipleRegistersResponse),
25-
(reg_msg.WriteSingleRegisterRequest, reg_msg.WriteSingleRegisterResponse),
26-
(bit_msg.WriteSingleCoilRequest, bit_msg.WriteSingleCoilResponse),
27-
(reg_msg.ReadWriteMultipleRegistersRequest, reg_msg.ReadWriteMultipleRegistersResponse),
28-
(diag_msg.DiagnosticBase, diag_msg.DiagnosticBase),
29-
(o_msg.ReadExceptionStatusRequest, o_msg.ReadExceptionStatusResponse),
30-
(o_msg.GetCommEventCounterRequest, o_msg.GetCommEventCounterResponse),
31-
(o_msg.GetCommEventLogRequest, o_msg.GetCommEventLogResponse),
32-
(o_msg.ReportDeviceIdRequest, o_msg.ReportDeviceIdResponse),
33-
(file_msg.ReadFileRecordRequest, file_msg.ReadFileRecordResponse),
34-
(file_msg.WriteFileRecordRequest, file_msg.WriteFileRecordResponse),
35-
(reg_msg.MaskWriteRegisterRequest, reg_msg.MaskWriteRegisterResponse),
36-
(file_msg.ReadFifoQueueRequest, file_msg.ReadFifoQueueResponse),
37-
(mei_msg.ReadDeviceInformationRequest, mei_msg.ReadDeviceInformationResponse),
38-
}
39-
40-
_pdu_sub_class_table: set[tuple[type[base.ModbusPDU], type[base.ModbusPDU]]] = {
41-
(diag_msg.ReturnQueryDataRequest, diag_msg.ReturnQueryDataResponse),
42-
(diag_msg.RestartCommunicationsOptionRequest, diag_msg.RestartCommunicationsOptionResponse),
43-
(diag_msg.ReturnDiagnosticRegisterRequest, diag_msg.ReturnDiagnosticRegisterResponse),
44-
(diag_msg.ChangeAsciiInputDelimiterRequest, diag_msg.ChangeAsciiInputDelimiterResponse),
45-
(diag_msg.ForceListenOnlyModeRequest, diag_msg.ForceListenOnlyModeResponse),
46-
(diag_msg.ClearCountersRequest, diag_msg.ClearCountersResponse),
47-
(diag_msg.ReturnBusMessageCountRequest, diag_msg.ReturnBusMessageCountResponse),
48-
(diag_msg.ReturnBusCommunicationErrorCountRequest, diag_msg.ReturnBusCommunicationErrorCountResponse),
49-
(diag_msg.ReturnBusExceptionErrorCountRequest, diag_msg.ReturnBusExceptionErrorCountResponse),
50-
(diag_msg.ReturnDeviceMessageCountRequest, diag_msg.ReturnDeviceMessageCountResponse),
51-
(diag_msg.ReturnDeviceNoResponseCountRequest, diag_msg.ReturnDeviceNoResponseCountResponse),
52-
(diag_msg.ReturnDeviceNAKCountRequest, diag_msg.ReturnDeviceNAKCountResponse),
53-
(diag_msg.ReturnDeviceBusyCountRequest, diag_msg.ReturnDeviceBusyCountResponse),
54-
(diag_msg.ReturnDeviceBusCharacterOverrunCountRequest, diag_msg.ReturnDeviceBusCharacterOverrunCountResponse),
55-
(diag_msg.ReturnIopOverrunCountRequest, diag_msg.ReturnIopOverrunCountResponse),
56-
(diag_msg.ClearOverrunCountRequest, diag_msg.ClearOverrunCountResponse),
57-
(diag_msg.GetClearModbusPlusRequest, diag_msg.GetClearModbusPlusResponse),
58-
(mei_msg.ReadDeviceInformationRequest, mei_msg.ReadDeviceInformationResponse),
59-
}
13+
pdu_class_table: dict[int, tuple[type[ModbusPDU], type[ModbusPDU]]] = {}
14+
pdu_sub_class_table: dict[int, dict[int, tuple[type[ModbusPDU], type[ModbusPDU]]]] = {}
6015

6116
def __init__(self, is_server: bool) -> None:
6217
"""Initialize function_tables."""
63-
inx = 0 if is_server else 1
64-
self.lookup: dict[int, type[base.ModbusPDU]] = {cl[inx].function_code: cl[inx] for cl in self._pdu_class_table}
65-
self.sub_lookup: dict[int, dict[int, type[base.ModbusPDU]]] = {}
66-
for f in self._pdu_sub_class_table:
67-
if (function_code := f[inx].function_code) not in self.sub_lookup:
68-
self.sub_lookup[function_code] = {f[inx].sub_function_code: f[inx]}
69-
else:
70-
self.sub_lookup[function_code][f[inx].sub_function_code] = f[inx]
18+
self.inx = 0 if is_server else 1
19+
20+
@classmethod
21+
def add_pdu(cls, req: type[ModbusPDU], resp: type[ModbusPDU]):
22+
"""Register request/response."""
23+
cls.pdu_class_table[req.function_code] = (req, resp)
7124

72-
def lookupPduClass(self, data: bytes) -> type[base.ModbusPDU] | None:
25+
@classmethod
26+
def add_sub_pdu(cls, req: type[ModbusPDU], resp: type[ModbusPDU]):
27+
"""Register request/response."""
28+
if req.function_code not in cls.pdu_sub_class_table:
29+
cls.pdu_sub_class_table[req.function_code] = {}
30+
cls.pdu_sub_class_table[req.function_code][req.sub_function_code] = (req, resp)
31+
32+
def lookupPduClass(self, data: bytes) -> type[ModbusPDU] | None:
7333
"""Use `function_code` to determine the class of the PDU."""
7434
func_code = int(data[1])
7535
if func_code & 0x80:
76-
return base.ExceptionResponse
36+
return ExceptionResponse
7737
if func_code == 0x2B: # mei message, sub_function_code is 1 byte
7838
sub_func_code = int(data[2])
79-
return self.sub_lookup[func_code].get(sub_func_code, None)
39+
if not (type_class := self.pdu_sub_class_table[func_code].get(sub_func_code, None)):
40+
return None
41+
return type_class[self.inx]
42+
8043
if func_code == 0x08: # diag message, sub_function_code is 2 bytes
8144
sub_func_code = int(data[3])
82-
return self.sub_lookup[func_code].get(sub_func_code, None)
83-
return self.lookup.get(func_code, None)
45+
if not (type_class := self.pdu_sub_class_table[func_code].get(sub_func_code, None)):
46+
return None
47+
return type_class[self.inx]
48+
if not (type_class := self.pdu_class_table.get(func_code, None)):
49+
return None
50+
return type_class[self.inx]
8451

85-
def register(self, custom_class: type[base.ModbusPDU]) -> None:
52+
def register(self, custom_class: type[ModbusPDU]) -> None:
8653
"""Register a function and sub function class with the decoder."""
87-
if not issubclass(custom_class, base.ModbusPDU):
54+
if not issubclass(custom_class, ModbusPDU):
8855
raise MessageRegisterException(
8956
f'"{custom_class.__class__.__name__}" is Not a valid Modbus Message'
9057
". Class needs to be derived from "
9158
"`pymodbus.pdu.ModbusPDU` "
9259
)
93-
self.lookup[custom_class.function_code] = custom_class
60+
self.pdu_class_table[custom_class.function_code] = (custom_class, custom_class)
9461
if custom_class.sub_function_code >= 0:
95-
if custom_class.function_code not in self.sub_lookup:
96-
self.sub_lookup[custom_class.function_code] = {}
97-
self.sub_lookup[custom_class.function_code][
62+
if custom_class.function_code not in self.pdu_sub_class_table:
63+
self.pdu_sub_class_table[custom_class.function_code] = {}
64+
self.pdu_sub_class_table[custom_class.function_code][
9865
custom_class.sub_function_code
99-
] = custom_class
66+
] = (custom_class, custom_class)
10067

101-
def decode(self, frame: bytes) -> base.ModbusPDU | None:
68+
def decode(self, frame: bytes) -> ModbusPDU | None:
10269
"""Decode a frame."""
10370
try:
10471
if (function_code := int(frame[0])) > 0x80:
105-
pdu_exp = base.ExceptionResponse(function_code & 0x7F)
72+
pdu_exp = ExceptionResponse(function_code & 0x7F)
10673
pdu_exp.decode(frame[1:])
10774
return pdu_exp
108-
if not (pdu_class := self.lookup.get(function_code, None)):
75+
if not (pdu_class := self.pdu_class_table.get(function_code, None)):
10976
Log.debug("decode PDU failed for function code {}", function_code)
11077
raise ModbusException(f"Unknown response {function_code}")
111-
pdu = pdu_class()
78+
pdu = pdu_class[self.inx]()
11279
pdu.decode(frame[1:])
11380
if pdu.sub_function_code >= 0:
114-
lookup = self.sub_lookup.get(pdu.function_code, {})
115-
if sub_class := lookup.get(pdu.sub_function_code, None):
116-
pdu = sub_class()
81+
lookup = self.pdu_sub_class_table.get(pdu.function_code, {})
82+
if type_class := lookup.get(pdu.sub_function_code, None):
83+
pdu = type_class[self.inx]()
11784
pdu.decode(frame[1:])
11885
Log.debug("decoded PDU function_code({} sub {}) -> {} ", pdu.function_code, pdu.sub_function_code, str(pdu))
11986
return pdu

pymodbus/pdu/diag_message.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pymodbus.constants import ModbusPlusOperation
88
from pymodbus.datastore import ModbusDeviceContext
99

10+
from .decoders import DecodePDU
1011
from .device import ModbusControlBlock
1112
from .pdu import ModbusPDU, pack_bitstring
1213

@@ -381,3 +382,22 @@ class GetClearModbusPlusResponse(DiagnosticBase):
381382
"""GetClearModbusPlusResponse."""
382383

383384
sub_function_code = 0x0015
385+
386+
DecodePDU.add_pdu(DiagnosticBase, DiagnosticBase)
387+
DecodePDU.add_sub_pdu(ReturnQueryDataRequest, ReturnQueryDataResponse)
388+
DecodePDU.add_sub_pdu(RestartCommunicationsOptionRequest, RestartCommunicationsOptionResponse)
389+
DecodePDU.add_sub_pdu(ReturnDiagnosticRegisterRequest, ReturnDiagnosticRegisterResponse)
390+
DecodePDU.add_sub_pdu(ChangeAsciiInputDelimiterRequest, ChangeAsciiInputDelimiterResponse)
391+
DecodePDU.add_sub_pdu(ForceListenOnlyModeRequest, ForceListenOnlyModeResponse)
392+
DecodePDU.add_sub_pdu(ClearCountersRequest, ClearCountersResponse)
393+
DecodePDU.add_sub_pdu(ReturnBusMessageCountRequest, ReturnBusMessageCountResponse)
394+
DecodePDU.add_sub_pdu(ReturnBusCommunicationErrorCountRequest, ReturnBusCommunicationErrorCountResponse)
395+
DecodePDU.add_sub_pdu(ReturnBusExceptionErrorCountRequest, ReturnBusExceptionErrorCountResponse)
396+
DecodePDU.add_sub_pdu(ReturnDeviceMessageCountRequest, ReturnDeviceMessageCountResponse)
397+
DecodePDU.add_sub_pdu(ReturnDeviceNoResponseCountRequest, ReturnDeviceNoResponseCountResponse)
398+
DecodePDU.add_sub_pdu(ReturnDeviceNAKCountRequest, ReturnDeviceNAKCountResponse)
399+
DecodePDU.add_sub_pdu(ReturnDeviceBusyCountRequest, ReturnDeviceBusyCountResponse)
400+
DecodePDU.add_sub_pdu(ReturnDeviceBusCharacterOverrunCountRequest, ReturnDeviceBusCharacterOverrunCountResponse)
401+
DecodePDU.add_sub_pdu(ReturnIopOverrunCountRequest, ReturnIopOverrunCountResponse)
402+
DecodePDU.add_sub_pdu(ClearOverrunCountRequest, ClearOverrunCountResponse)
403+
DecodePDU.add_sub_pdu(GetClearModbusPlusRequest, GetClearModbusPlusResponse)

pymodbus/pdu/file_message.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pymodbus.datastore import ModbusDeviceContext
88
from pymodbus.exceptions import ModbusException
99

10+
from .decoders import DecodePDU
1011
from .pdu import ModbusPDU
1112

1213

@@ -276,3 +277,7 @@ def decode(self, data: bytes) -> None:
276277
for index in range(0, count - 4):
277278
idx = 4 + index * 2
278279
self.values.append(struct.unpack(">H", data[idx : idx + 2])[0])
280+
281+
DecodePDU.add_pdu(ReadFileRecordRequest, ReadFileRecordResponse)
282+
DecodePDU.add_pdu(WriteFileRecordRequest, WriteFileRecordResponse)
283+
DecodePDU.add_pdu(ReadFifoQueueRequest, ReadFifoQueueResponse)

pymodbus/pdu/mei_message.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pymodbus.constants import DeviceInformation, ExcCodes, MoreData
77
from pymodbus.datastore import ModbusDeviceContext
88

9+
from .decoders import DecodePDU
910
from .device import DeviceInformationFactory, ModbusControlBlock
1011
from .exceptionresponse import ExceptionResponse
1112
from .pdu import ModbusPDU
@@ -153,3 +154,6 @@ def decode(self, data: bytes) -> None:
153154
self.information[object_id],
154155
data[count - object_length : count],
155156
]
157+
158+
DecodePDU.add_pdu(ReadDeviceInformationRequest, ReadDeviceInformationResponse)
159+
DecodePDU.add_sub_pdu(ReadDeviceInformationRequest, ReadDeviceInformationResponse)

pymodbus/pdu/other_message.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pymodbus.constants import ModbusStatus
77
from pymodbus.datastore import ModbusDeviceContext
88

9+
from .decoders import DecodePDU
910
from .device import DeviceInformationFactory, ModbusControlBlock
1011
from .pdu import ModbusPDU
1112

@@ -208,3 +209,8 @@ def decode(self, data: bytes) -> None:
208209
self.identifier = data[1 : self.byte_count + 1]
209210
status = int(data[-1])
210211
self.status = status == ID_ON
212+
213+
DecodePDU.add_pdu(ReadExceptionStatusRequest, ReadExceptionStatusResponse)
214+
DecodePDU.add_pdu(GetCommEventCounterRequest, GetCommEventCounterResponse)
215+
DecodePDU.add_pdu(GetCommEventLogRequest, GetCommEventLogResponse)
216+
DecodePDU.add_pdu(ReportDeviceIdRequest, ReportDeviceIdResponse)

pymodbus/pdu/register_message.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pymodbus.datastore import ModbusDeviceContext
1111
from pymodbus.exceptions import ModbusIOException
1212

13+
from .decoders import DecodePDU
1314
from .exceptionresponse import ExceptionResponse
1415
from .pdu import ModbusPDU
1516

@@ -381,3 +382,10 @@ def encode(self) -> bytes:
381382
def decode(self, data: bytes) -> None:
382383
"""Decode a the response."""
383384
self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data[:6])
385+
386+
DecodePDU.add_pdu(ReadHoldingRegistersRequest, ReadHoldingRegistersResponse)
387+
DecodePDU.add_pdu(ReadInputRegistersRequest, ReadInputRegistersResponse)
388+
DecodePDU.add_pdu(WriteMultipleRegistersRequest, WriteMultipleRegistersResponse)
389+
DecodePDU.add_pdu(WriteSingleRegisterRequest, WriteSingleRegisterResponse)
390+
DecodePDU.add_pdu(ReadWriteMultipleRegistersRequest, ReadWriteMultipleRegistersResponse)
391+
DecodePDU.add_pdu(MaskWriteRegisterRequest, MaskWriteRegisterResponse)

pymodbus/server/simulator/http_server.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class CallTypeResponse:
7171
clear_after: int = 1
7272

7373

74-
class ModbusSimulatorServer: # pylint: disable=too-many-instance-attributes
74+
class ModbusSimulatorServer:
7575
"""**ModbusSimulatorServer**.
7676
7777
:param modbus_server: Server name in json file (default: "server")
@@ -201,7 +201,6 @@ def __init__(
201201
self.refresh_rate = 0
202202
self.register_filter: list[int] = []
203203
self.call_list: list[CallTracer] = []
204-
self.request_lookup = DecodePDU(True).lookup
205204
self.call_monitor = CallTypeMonitor()
206205
self.call_response = CallTypeResponse()
207206
app_key = getattr(web, 'AppKey', str) # fall back to str for aiohttp < 3.9.0
@@ -376,13 +375,13 @@ def build_html_calls(self, params: dict, html: str) -> str:
376375
else ""
377376
)
378377
function_codes = ""
379-
for function in self.request_lookup.values():
378+
for (req, _resp) in DecodePDU.pdu_class_table.values():
380379
selected = (
381380
"selected"
382-
if function.function_code == self.call_monitor.function
381+
if req.function_code == self.call_monitor.function
383382
else ""
384383
)
385-
function_codes += f"<option value={function.function_code} {selected}>function code name</option>"
384+
function_codes += f"<option value={req.function_code} {selected}>function code name</option>"
386385
simulation_action = (
387386
"ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else ""
388387
)
@@ -392,9 +391,7 @@ def build_html_calls(self, params: dict, html: str) -> str:
392391
del self.call_list[0]
393392
call_rows = ""
394393
for entry in reversed(self.call_list):
395-
# req_obj = self.request_lookup[entry[1]]
396394
call_rows += f"<tr><td>{entry.call} - {entry.fc}</td><td>{entry.address}</td><td>{entry.count}</td><td>{entry.data.decode()}</td></tr>"
397-
# line += req_obj.funcion_code_name
398395
new_html = (
399396
html.replace("<!--SIMULATION_ACTIVE-->", simulation_action)
400397
.replace("FUNCTION_RANGE_START", range_start_html)
@@ -543,11 +540,11 @@ def build_json_calls(self, params: dict) -> dict:
543540
)
544541

545542
function_codes = []
546-
for function in self.request_lookup.values():
543+
for (req, _resp) in DecodePDU.pdu_class_table.values():
547544
function_codes.append({
548-
"value": function.function_code,
545+
"value": req.function_code,
549546
"text": "function code name",
550-
"selected": function.function_code == self.call_monitor.function
547+
"selected": req.function_code == self.call_monitor.function
551548
})
552549

553550
simulation_action = "ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else ""

0 commit comments

Comments
 (0)