pyModbusTCP 0.2.2__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/CHANGES +11 -0
- {pyModbusTCP-0.2.2/pyModbusTCP.egg-info → pymodbustcp-0.3.1}/PKG-INFO +11 -3
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/README.rst +1 -1
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/pyModbusTCP/__init__.py +4 -2
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/pyModbusTCP/client.py +47 -74
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/pyModbusTCP/constants.py +1 -1
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/pyModbusTCP/server.py +20 -15
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1/pyModbusTCP.egg-info}/PKG-INFO +11 -3
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/pyModbusTCP.egg-info/SOURCES.txt +5 -1
- pymodbustcp-0.3.1/tests/test_client.py +58 -0
- pymodbustcp-0.3.1/tests/test_client_server.py +243 -0
- pymodbustcp-0.3.1/tests/test_server.py +43 -0
- pymodbustcp-0.3.1/tests/test_utils.py +143 -0
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/LICENSE +0 -0
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/MANIFEST.in +0 -0
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/pyModbusTCP/utils.py +0 -0
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/pyModbusTCP.egg-info/dependency_links.txt +0 -0
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/pyModbusTCP.egg-info/top_level.txt +0 -0
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/setup.cfg +0 -0
- {pyModbusTCP-0.2.2 → pymodbustcp-0.3.1}/setup.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
Revision history for pyModbusTCP
|
|
2
2
|
|
|
3
|
+
0.3.1 2026-09-29
|
|
4
|
+
|
|
5
|
+
- fix ModbusServer: debug messages now include OSError exceptions produced by getpeername (thanks to Pär Åhlund).
|
|
6
|
+
- fix ModbusClient: prevent incomplete frame transmission by using sendall (thanks to aamir-azeez).
|
|
7
|
+
- updated compatibility test (python versions): remove 3.8/3.9, add 3.13/3.14.
|
|
8
|
+
|
|
9
|
+
0.3.0 2024-09-04
|
|
10
|
+
|
|
11
|
+
- pyModbusTCP.client: now use the standard logging method as in the server part.
|
|
12
|
+
- ModbusClient: debug flag is removed (see examples/client_debug.py).
|
|
13
|
+
|
|
3
14
|
0.2.2 2024-07-31
|
|
4
15
|
|
|
5
16
|
- fix ModbusServer: wrong check of discrete inputs length in DataBank (thanks to OTnetproj).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pyModbusTCP
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A simple Modbus/TCP library for Python
|
|
5
5
|
Home-page: https://github.com/sourceperl/pyModbusTCP
|
|
6
6
|
Author: Loic Lefebvre
|
|
@@ -8,6 +8,14 @@ Author-email: loic.celine@free.fr
|
|
|
8
8
|
License: MIT
|
|
9
9
|
Platform: any
|
|
10
10
|
License-File: LICENSE
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: author-email
|
|
13
|
+
Dynamic: description
|
|
14
|
+
Dynamic: home-page
|
|
15
|
+
Dynamic: license
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
Dynamic: platform
|
|
18
|
+
Dynamic: summary
|
|
11
19
|
|
|
12
20
|
.. |badge_tests| image:: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master
|
|
13
21
|
:target: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml
|
|
@@ -26,7 +34,7 @@ Since version 0.1.0, a server is also available.
|
|
|
26
34
|
Tests
|
|
27
35
|
-----
|
|
28
36
|
|
|
29
|
-
The module is currently test on Python 3.
|
|
37
|
+
The module is currently test on Python 3.10, 3.11, 3.12, 3.13 and 3.14.
|
|
30
38
|
|
|
31
39
|
For Linux, Mac OS and Windows.
|
|
32
40
|
|
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
# 1,2,4,5,6 (Class 1)
|
|
7
7
|
# 15,23,43
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import logging
|
|
10
10
|
|
|
11
|
+
from .constants import VERSION
|
|
11
12
|
|
|
12
|
-
__all__ = ['constants', 'client', 'server', 'utils']
|
|
13
13
|
__title__ = 'pyModbusTCP'
|
|
14
14
|
__description__ = 'A simple Modbus/TCP library for Python.'
|
|
15
15
|
__url__ = 'https://github.com/sourceperl/pyModbusTCP'
|
|
16
16
|
__version__ = VERSION
|
|
17
17
|
__license__ = 'MIT'
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
""" pyModbusTCP Client """
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from .constants import READ_COILS, READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS, READ_INPUT_REGISTERS, \
|
|
5
|
-
WRITE_MULTIPLE_COILS, WRITE_MULTIPLE_REGISTERS, WRITE_SINGLE_COIL, WRITE_SINGLE_REGISTER, \
|
|
6
|
-
WRITE_READ_MULTIPLE_REGISTERS, ENCAPSULATED_INTERFACE_TRANSPORT, MEI_TYPE_READ_DEVICE_ID, \
|
|
7
|
-
MB_ERR_TXT, MB_NO_ERR, MB_SEND_ERR, MB_RECV_ERR, MB_TIMEOUT_ERR, MB_EXCEPT_ERR, MB_CONNECT_ERR, \
|
|
8
|
-
EXP_TXT, EXP_DETAILS, EXP_NONE, MB_SOCK_CLOSE_ERR, VERSION
|
|
9
|
-
from .utils import byte_length, set_bit, valid_host
|
|
3
|
+
import logging
|
|
10
4
|
import random
|
|
11
5
|
import socket
|
|
12
|
-
from socket import AF_UNSPEC, SOCK_STREAM
|
|
13
6
|
import struct
|
|
7
|
+
from binascii import hexlify
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from socket import AF_UNSPEC, SOCK_STREAM
|
|
14
10
|
from typing import Dict
|
|
15
11
|
|
|
12
|
+
from .constants import (ENCAPSULATED_INTERFACE_TRANSPORT, EXP_DETAILS,
|
|
13
|
+
EXP_NONE, EXP_TXT, MB_CONNECT_ERR, MB_ERR_TXT,
|
|
14
|
+
MB_EXCEPT_ERR, MB_NO_ERR, MB_RECV_ERR, MB_SEND_ERR,
|
|
15
|
+
MB_SOCK_CLOSE_ERR, MB_TIMEOUT_ERR,
|
|
16
|
+
MEI_TYPE_READ_DEVICE_ID, READ_COILS,
|
|
17
|
+
READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS,
|
|
18
|
+
READ_INPUT_REGISTERS, VERSION, WRITE_MULTIPLE_COILS,
|
|
19
|
+
WRITE_MULTIPLE_REGISTERS,
|
|
20
|
+
WRITE_READ_MULTIPLE_REGISTERS, WRITE_SINGLE_COIL,
|
|
21
|
+
WRITE_SINGLE_REGISTER)
|
|
22
|
+
from .utils import byte_length, set_bit, valid_host
|
|
23
|
+
|
|
24
|
+
# add a logger for pyModbusTCP.client
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
16
27
|
|
|
17
28
|
@dataclass
|
|
18
29
|
class DeviceIdentificationResponse:
|
|
19
30
|
"""Modbus TCP client function read_device_identification() response struct.
|
|
20
|
-
|
|
31
|
+
|
|
21
32
|
:param conformity_level: this represents supported access and object type
|
|
22
33
|
:type conformity_level: int
|
|
23
34
|
:param more_follows: for stream request can be set to 0xff if other objects are available (0x00 in other cases)
|
|
@@ -76,8 +87,7 @@ class ModbusClient:
|
|
|
76
87
|
def __init__(self, code):
|
|
77
88
|
self.code = code
|
|
78
89
|
|
|
79
|
-
def __init__(self, host='localhost', port=502, unit_id=1, timeout=30.0,
|
|
80
|
-
debug=False, auto_open=True, auto_close=False):
|
|
90
|
+
def __init__(self, host='localhost', port=502, unit_id=1, timeout=30.0, auto_open=True, auto_close=False):
|
|
81
91
|
"""Constructor.
|
|
82
92
|
|
|
83
93
|
:param host: hostname or IPv4/IPv6 address server address
|
|
@@ -88,8 +98,6 @@ class ModbusClient:
|
|
|
88
98
|
:type unit_id: int
|
|
89
99
|
:param timeout: socket timeout in seconds
|
|
90
100
|
:type timeout: float
|
|
91
|
-
:param debug: debug state
|
|
92
|
-
:type debug: bool
|
|
93
101
|
:param auto_open: auto TCP connect
|
|
94
102
|
:type auto_open: bool
|
|
95
103
|
:param auto_close: auto TCP close)
|
|
@@ -103,7 +111,6 @@ class ModbusClient:
|
|
|
103
111
|
self._port = None
|
|
104
112
|
self._unit_id = None
|
|
105
113
|
self._timeout = None
|
|
106
|
-
self._debug = None
|
|
107
114
|
self._auto_open = None
|
|
108
115
|
self._auto_close = None
|
|
109
116
|
self._sock = socket.socket()
|
|
@@ -117,13 +124,12 @@ class ModbusClient:
|
|
|
117
124
|
self.port = port
|
|
118
125
|
self.unit_id = unit_id
|
|
119
126
|
self.timeout = timeout
|
|
120
|
-
self.debug = debug
|
|
121
127
|
self.auto_open = auto_open
|
|
122
128
|
self.auto_close = auto_close
|
|
123
129
|
|
|
124
130
|
def __repr__(self):
|
|
125
|
-
r_str = 'ModbusClient(host=\'%s\', port=%d, unit_id=%d, timeout=%.2f,
|
|
126
|
-
r_str %= (self.host, self.port, self.unit_id, self.timeout, self.
|
|
131
|
+
r_str = 'ModbusClient(host=\'%s\', port=%d, unit_id=%d, timeout=%.2f, auto_open=%s, auto_close=%s)'
|
|
132
|
+
r_str %= (self.host, self.port, self.unit_id, self.timeout, self.auto_open, self.auto_close)
|
|
127
133
|
return r_str
|
|
128
134
|
|
|
129
135
|
def __del__(self):
|
|
@@ -248,16 +254,6 @@ class ModbusClient:
|
|
|
248
254
|
# can't be set
|
|
249
255
|
raise ValueError('timeout can\'t be set (valid between 0 and 3600)')
|
|
250
256
|
|
|
251
|
-
@property
|
|
252
|
-
def debug(self):
|
|
253
|
-
"""Get or set the debug flag (True = turn on)."""
|
|
254
|
-
return self._debug
|
|
255
|
-
|
|
256
|
-
@debug.setter
|
|
257
|
-
def debug(self, value):
|
|
258
|
-
# enforce type
|
|
259
|
-
self._debug = bool(value)
|
|
260
|
-
|
|
261
257
|
@property
|
|
262
258
|
def auto_open(self):
|
|
263
259
|
"""Get or set automatic TCP connect mode (True = turn on)."""
|
|
@@ -521,7 +517,8 @@ class ModbusClient:
|
|
|
521
517
|
raise ValueError('object_id out of range (valid from 0 to 255)')
|
|
522
518
|
# make request
|
|
523
519
|
try:
|
|
524
|
-
tx_pdu = struct.pack('BBBB', ENCAPSULATED_INTERFACE_TRANSPORT,
|
|
520
|
+
tx_pdu = struct.pack('BBBB', ENCAPSULATED_INTERFACE_TRANSPORT,
|
|
521
|
+
MEI_TYPE_READ_DEVICE_ID, read_code, object_id)
|
|
525
522
|
rx_pdu = self._req_pdu(tx_pdu=tx_pdu, rx_min_len=7)
|
|
526
523
|
# init response object for populate it
|
|
527
524
|
response = DeviceIdentificationResponse()
|
|
@@ -778,7 +775,7 @@ class ModbusClient:
|
|
|
778
775
|
raise ModbusClient._NetworkError(MB_SOCK_CLOSE_ERR, 'try to send on a close socket')
|
|
779
776
|
# send
|
|
780
777
|
try:
|
|
781
|
-
self._sock.
|
|
778
|
+
self._sock.sendall(frame)
|
|
782
779
|
except socket.timeout:
|
|
783
780
|
self._sock.close()
|
|
784
781
|
raise ModbusClient._NetworkError(MB_TIMEOUT_ERR, 'timeout error')
|
|
@@ -800,7 +797,7 @@ class ModbusClient:
|
|
|
800
797
|
# send frame with error check
|
|
801
798
|
self._send(tx_frame)
|
|
802
799
|
# debug
|
|
803
|
-
self.
|
|
800
|
+
self._on_tx_rx(frame=tx_frame, is_tx=True)
|
|
804
801
|
|
|
805
802
|
def _recv(self, size):
|
|
806
803
|
"""Receive data over current socket.
|
|
@@ -856,7 +853,7 @@ class ModbusClient:
|
|
|
856
853
|
# checking error status of fields
|
|
857
854
|
if f_transaction_err or f_protocol_err or f_length_err or f_unit_id_err:
|
|
858
855
|
self.close()
|
|
859
|
-
self.
|
|
856
|
+
self._on_tx_rx(frame=rx_mbap, is_tx=False)
|
|
860
857
|
raise ModbusClient._NetworkError(MB_RECV_ERR, 'MBAP checking error')
|
|
861
858
|
# recv PDU
|
|
862
859
|
rx_pdu = self._recv_all(f_length - 1)
|
|
@@ -864,7 +861,7 @@ class ModbusClient:
|
|
|
864
861
|
if self.auto_close:
|
|
865
862
|
self.close()
|
|
866
863
|
# dump frame
|
|
867
|
-
self.
|
|
864
|
+
self._on_tx_rx(frame=rx_mbap + rx_pdu, is_tx=False)
|
|
868
865
|
# body decode
|
|
869
866
|
# check PDU length for global minimal frame (an except frame: func code + exp code)
|
|
870
867
|
if len(rx_pdu) < 2:
|
|
@@ -929,45 +926,21 @@ class ModbusClient:
|
|
|
929
926
|
if isinstance(_except, ModbusClient._ModbusExcept):
|
|
930
927
|
self._last_error = MB_EXCEPT_ERR
|
|
931
928
|
self._last_except = _except.code
|
|
932
|
-
self._debug_msg('modbus exception (code
|
|
933
|
-
|
|
934
|
-
def _debug_msg(self, msg):
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
"""
|
|
951
|
-
if self.debug:
|
|
952
|
-
self._pretty_dump(label, frame)
|
|
953
|
-
|
|
954
|
-
@staticmethod
|
|
955
|
-
def _pretty_dump(label, frame):
|
|
956
|
-
"""Dump a modbus frame.
|
|
957
|
-
|
|
958
|
-
modbus/TCP format: [MBAP] PDU
|
|
959
|
-
|
|
960
|
-
:param label: head label
|
|
961
|
-
:type label: str
|
|
962
|
-
:param frame: modbus frame
|
|
963
|
-
:type frame: bytes
|
|
964
|
-
"""
|
|
965
|
-
# split data string items to a list of hex value
|
|
966
|
-
dump = ['%02X' % c for c in frame]
|
|
967
|
-
# format message
|
|
968
|
-
dump_mbap = ' '.join(dump[0:7])
|
|
969
|
-
dump_pdu = ' '.join(dump[7:])
|
|
970
|
-
msg = '[%s] %s' % (dump_mbap, dump_pdu)
|
|
971
|
-
# print result
|
|
972
|
-
print(label)
|
|
973
|
-
print(msg)
|
|
929
|
+
self._debug_msg(f'modbus exception (code {self.last_except} "{self.last_error_as_txt}")')
|
|
930
|
+
|
|
931
|
+
def _debug_msg(self, msg: str):
|
|
932
|
+
logger.debug(f'({self.host}:{self.port}:{self.unit_id}) {msg}')
|
|
933
|
+
|
|
934
|
+
def _on_tx_rx(self, frame: bytes, is_tx: bool):
|
|
935
|
+
# format a log message
|
|
936
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
937
|
+
type_s = 'Tx' if is_tx else 'Rx'
|
|
938
|
+
mbap_s = hexlify(frame[0:7], sep=' ').upper().decode()
|
|
939
|
+
pdu_s = hexlify(frame[7:], sep=' ').upper().decode()
|
|
940
|
+
self._debug_msg(f'{type_s} [{mbap_s}] {pdu_s}')
|
|
941
|
+
# notify user
|
|
942
|
+
self.on_tx_rx(frame=frame, is_tx=is_tx)
|
|
943
|
+
|
|
944
|
+
def on_tx_rx(self, frame: bytes, is_tx: bool):
|
|
945
|
+
"""Call for each Tx/Rx (for user purposes)."""
|
|
946
|
+
pass
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
""" pyModbusTCP Server """
|
|
2
2
|
|
|
3
|
-
from .constants import READ_COILS, READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS, READ_INPUT_REGISTERS, \
|
|
4
|
-
WRITE_MULTIPLE_COILS, WRITE_MULTIPLE_REGISTERS, WRITE_SINGLE_COIL, WRITE_SINGLE_REGISTER, \
|
|
5
|
-
EXP_NONE, EXP_ILLEGAL_FUNCTION, EXP_DATA_ADDRESS, EXP_DATA_VALUE, WRITE_READ_MULTIPLE_REGISTERS, \
|
|
6
|
-
ENCAPSULATED_INTERFACE_TRANSPORT, MEI_TYPE_READ_DEVICE_ID, MAX_PDU_SIZE
|
|
7
|
-
from .utils import test_bit, set_bit
|
|
8
3
|
import logging
|
|
9
4
|
import socket
|
|
10
|
-
from socketserver import BaseRequestHandler, ThreadingTCPServer
|
|
11
5
|
import struct
|
|
12
|
-
from
|
|
6
|
+
from socketserver import BaseRequestHandler, ThreadingTCPServer
|
|
7
|
+
from threading import Event, Lock, Thread
|
|
13
8
|
from warnings import warn
|
|
14
9
|
|
|
10
|
+
from .constants import (ENCAPSULATED_INTERFACE_TRANSPORT, EXP_DATA_ADDRESS,
|
|
11
|
+
EXP_DATA_VALUE, EXP_ILLEGAL_FUNCTION, EXP_NONE,
|
|
12
|
+
MAX_PDU_SIZE, MEI_TYPE_READ_DEVICE_ID, READ_COILS,
|
|
13
|
+
READ_DISCRETE_INPUTS, READ_HOLDING_REGISTERS,
|
|
14
|
+
READ_INPUT_REGISTERS, WRITE_MULTIPLE_COILS,
|
|
15
|
+
WRITE_MULTIPLE_REGISTERS,
|
|
16
|
+
WRITE_READ_MULTIPLE_REGISTERS, WRITE_SINGLE_COIL,
|
|
17
|
+
WRITE_SINGLE_REGISTER)
|
|
18
|
+
from .utils import set_bit, test_bit
|
|
19
|
+
|
|
15
20
|
# add a logger for pyModbusTCP.server
|
|
16
|
-
logger = logging.getLogger(
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class DataBank:
|
|
@@ -819,14 +824,14 @@ class ModbusServer:
|
|
|
819
824
|
self.request.settimeout(1.0)
|
|
820
825
|
|
|
821
826
|
def handle(self):
|
|
822
|
-
# try/except end current thread on ModbusServer._InternalError or socket.error
|
|
827
|
+
# try/except: end current thread on ModbusServer._InternalError, OSError or socket.error
|
|
823
828
|
# this also close the current TCP session associated with it
|
|
824
|
-
# init and update server info structure
|
|
825
|
-
session_data = ModbusServer.SessionData()
|
|
826
|
-
(session_data.client.address, session_data.client.port) = self.request.getpeername()
|
|
827
|
-
# debug message
|
|
828
|
-
logger.debug('Accept new connection from %r', session_data.client)
|
|
829
829
|
try:
|
|
830
|
+
# init and update server info structure
|
|
831
|
+
session_data = ModbusServer.SessionData()
|
|
832
|
+
(session_data.client.address, session_data.client.port) = self.request.getpeername()
|
|
833
|
+
# debug message
|
|
834
|
+
logger.debug('accept new connection from %r', session_data.client)
|
|
830
835
|
# main processing loop
|
|
831
836
|
while True:
|
|
832
837
|
# init session data for new request
|
|
@@ -841,7 +846,7 @@ class ModbusServer:
|
|
|
841
846
|
self.server.engine(session_data)
|
|
842
847
|
# send the tx pdu with the last rx mbap (only length field change)
|
|
843
848
|
self._send_all(session_data.response.raw)
|
|
844
|
-
except (ModbusServer.Error, socket.error) as e:
|
|
849
|
+
except (ModbusServer.Error, OSError, socket.error) as e:
|
|
845
850
|
# debug message
|
|
846
851
|
logger.debug('Exception during request handling: %r', e)
|
|
847
852
|
# on main loop except: exit from it and cleanly close the current socket
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pyModbusTCP
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A simple Modbus/TCP library for Python
|
|
5
5
|
Home-page: https://github.com/sourceperl/pyModbusTCP
|
|
6
6
|
Author: Loic Lefebvre
|
|
@@ -8,6 +8,14 @@ Author-email: loic.celine@free.fr
|
|
|
8
8
|
License: MIT
|
|
9
9
|
Platform: any
|
|
10
10
|
License-File: LICENSE
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: author-email
|
|
13
|
+
Dynamic: description
|
|
14
|
+
Dynamic: home-page
|
|
15
|
+
Dynamic: license
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
Dynamic: platform
|
|
18
|
+
Dynamic: summary
|
|
11
19
|
|
|
12
20
|
.. |badge_tests| image:: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master
|
|
13
21
|
:target: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml
|
|
@@ -26,7 +34,7 @@ Since version 0.1.0, a server is also available.
|
|
|
26
34
|
Tests
|
|
27
35
|
-----
|
|
28
36
|
|
|
29
|
-
The module is currently test on Python 3.
|
|
37
|
+
The module is currently test on Python 3.10, 3.11, 3.12, 3.13 and 3.14.
|
|
30
38
|
|
|
31
39
|
For Linux, Mac OS and Windows.
|
|
32
40
|
|
|
@@ -12,4 +12,8 @@ pyModbusTCP/utils.py
|
|
|
12
12
|
pyModbusTCP.egg-info/PKG-INFO
|
|
13
13
|
pyModbusTCP.egg-info/SOURCES.txt
|
|
14
14
|
pyModbusTCP.egg-info/dependency_links.txt
|
|
15
|
-
pyModbusTCP.egg-info/top_level.txt
|
|
15
|
+
pyModbusTCP.egg-info/top_level.txt
|
|
16
|
+
tests/test_client.py
|
|
17
|
+
tests/test_client_server.py
|
|
18
|
+
tests/test_server.py
|
|
19
|
+
tests/test_utils.py
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
""" Test of pyModbusTCP.ModbusClient """
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from pyModbusTCP.client import ModbusClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestModbusClient(unittest.TestCase):
|
|
8
|
+
""" ModbusClient tests class. """
|
|
9
|
+
|
|
10
|
+
def test_host(self):
|
|
11
|
+
"""Test of host property."""
|
|
12
|
+
# default value
|
|
13
|
+
self.assertEqual(ModbusClient().host, 'localhost')
|
|
14
|
+
# should raise ValueError for bad value
|
|
15
|
+
self.assertRaises(ValueError, ModbusClient, host='wrong@host')
|
|
16
|
+
self.assertRaises(ValueError, ModbusClient, host='::notip:1')
|
|
17
|
+
# shouldn't raise ValueError for valid value
|
|
18
|
+
try:
|
|
19
|
+
[ModbusClient(host=h) for h in ['CamelCaseHost', 'plc-1.net', 'my.good.host',
|
|
20
|
+
'_test.example.com', '42.example.com',
|
|
21
|
+
'127.0.0.1', '::1']]
|
|
22
|
+
except ValueError:
|
|
23
|
+
self.fail('ModbusClient.host property raised ValueError unexpectedly')
|
|
24
|
+
|
|
25
|
+
def test_port(self):
|
|
26
|
+
"""Test of port property."""
|
|
27
|
+
# default value
|
|
28
|
+
self.assertEqual(ModbusClient().port, 502)
|
|
29
|
+
# should raise an exception for bad value
|
|
30
|
+
self.assertRaises(TypeError, ModbusClient, port='amsterdam')
|
|
31
|
+
self.assertRaises(ValueError, ModbusClient, port=-1)
|
|
32
|
+
# shouldn't raise ValueError for valid value
|
|
33
|
+
try:
|
|
34
|
+
ModbusClient(port=5020)
|
|
35
|
+
except ValueError:
|
|
36
|
+
self.fail('ModbusClient.port property raised ValueError unexpectedly')
|
|
37
|
+
|
|
38
|
+
def test_unit_id(self):
|
|
39
|
+
"""Test of unit_id property."""
|
|
40
|
+
# default value
|
|
41
|
+
self.assertEqual(ModbusClient().unit_id, 1)
|
|
42
|
+
# should raise an exception for bad unit_id
|
|
43
|
+
self.assertRaises(TypeError, ModbusClient, unit_id='@')
|
|
44
|
+
self.assertRaises(ValueError, ModbusClient, unit_id=420)
|
|
45
|
+
# shouldn't raise ValueError for valid value
|
|
46
|
+
try:
|
|
47
|
+
ModbusClient(port=5020)
|
|
48
|
+
except ValueError:
|
|
49
|
+
self.fail('ModbusClient.port property raised ValueError unexpectedly')
|
|
50
|
+
|
|
51
|
+
def test_misc(self):
|
|
52
|
+
"""Check of misc default values."""
|
|
53
|
+
self.assertEqual(ModbusClient().auto_open, True)
|
|
54
|
+
self.assertEqual(ModbusClient().auto_close, False)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
unittest.main()
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
""" Test of pyModbusTCP client-server interaction """
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import Mock
|
|
6
|
+
from random import randint, getrandbits, choice
|
|
7
|
+
from string import ascii_letters
|
|
8
|
+
from pyModbusTCP.server import ModbusServer, DeviceIdentification
|
|
9
|
+
from pyModbusTCP.client import ModbusClient, DeviceIdentificationResponse
|
|
10
|
+
from pyModbusTCP.constants import SUPPORTED_FUNCTION_CODES, \
|
|
11
|
+
EXP_NONE, EXP_ILLEGAL_FUNCTION, EXP_DATA_ADDRESS, EXP_DATA_VALUE, MB_NO_ERR, MB_EXCEPT_ERR, \
|
|
12
|
+
MB_SEND_ERR, MB_TIMEOUT_ERR
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# some const
|
|
16
|
+
MAX_READABLE_REGS = 125
|
|
17
|
+
MAX_WRITABLE_REGS = 123
|
|
18
|
+
MAX_WRITE_READ_REGS = 121
|
|
19
|
+
MAX_READABLE_BITS = 2000
|
|
20
|
+
MAX_WRITABLE_BITS = 1968
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestClientServer(unittest.TestCase):
|
|
24
|
+
""" Client-server interaction test class. """
|
|
25
|
+
|
|
26
|
+
def setUp(self):
|
|
27
|
+
"""Init client-server for test_xxx methods."""
|
|
28
|
+
# modbus server
|
|
29
|
+
self.server = ModbusServer(port=5020, no_block=True)
|
|
30
|
+
self.server.start()
|
|
31
|
+
# modbus client
|
|
32
|
+
self.client = ModbusClient(port=5020)
|
|
33
|
+
self.client.open()
|
|
34
|
+
|
|
35
|
+
def tearDown(self):
|
|
36
|
+
"""Cleanning after test."""
|
|
37
|
+
self.client.close()
|
|
38
|
+
self.server.stop()
|
|
39
|
+
|
|
40
|
+
def test_request_with_short_send(self):
|
|
41
|
+
"""Complete requests when a socket send accepts only a prefix."""
|
|
42
|
+
self.client.timeout = 1.0
|
|
43
|
+
self.assertTrue(self.client.open())
|
|
44
|
+
sock = self.client._sock
|
|
45
|
+
self.client._sock = Mock(wraps=sock)
|
|
46
|
+
self.client._sock.send.side_effect = lambda data: sock.send(data[:3])
|
|
47
|
+
self.assertEqual(self.client.read_holding_registers(0), [0])
|
|
48
|
+
registers = list(range(MAX_WRITABLE_REGS))
|
|
49
|
+
self.assertTrue(self.client.write_multiple_registers(0, registers))
|
|
50
|
+
self.assertEqual(self.client.read_holding_registers(0, len(registers)), registers)
|
|
51
|
+
|
|
52
|
+
def test_request_send_errors(self):
|
|
53
|
+
"""Send failures keep their error codes and close the connection."""
|
|
54
|
+
for error, code in [(socket.timeout(), MB_TIMEOUT_ERR), (OSError(), MB_SEND_ERR)]:
|
|
55
|
+
with self.subTest(error=type(error).__name__):
|
|
56
|
+
self.assertTrue(self.client.open())
|
|
57
|
+
sock = Mock(wraps=self.client._sock)
|
|
58
|
+
sock.send.side_effect = error
|
|
59
|
+
sock.sendall.side_effect = error
|
|
60
|
+
self.client._sock = sock
|
|
61
|
+
self.assertIsNone(self.client.read_holding_registers(0))
|
|
62
|
+
self.assertEqual(self.client.last_error, code)
|
|
63
|
+
self.assertFalse(self.client.is_open)
|
|
64
|
+
self.assertEqual(sock.send.call_count + sock.sendall.call_count, 1)
|
|
65
|
+
|
|
66
|
+
def test_default_startup_values(self):
|
|
67
|
+
"""Some read at random address to test startup values."""
|
|
68
|
+
for addr in [randint(0, 0xffff) for _ in range(100)]:
|
|
69
|
+
self.assertEqual(self.client.read_coils(addr), [False])
|
|
70
|
+
self.assertEqual(self.client.read_discrete_inputs(addr), [False])
|
|
71
|
+
self.assertEqual(self.client.read_holding_registers(addr), [0])
|
|
72
|
+
self.assertEqual(self.client.read_input_registers(addr), [0])
|
|
73
|
+
|
|
74
|
+
def test_read_write_requests(self):
|
|
75
|
+
"""Test standard modbus functions."""
|
|
76
|
+
# coils
|
|
77
|
+
for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_WRITABLE_BITS]:
|
|
78
|
+
# coils space: single read/write
|
|
79
|
+
bit = bool(getrandbits(1))
|
|
80
|
+
self.assertEqual(self.client.write_single_coil(addr, bit), True)
|
|
81
|
+
self.assertEqual(self.client.read_coils(addr), [bit])
|
|
82
|
+
# coils space: multiple read/write at min size
|
|
83
|
+
bits_l = [bool(getrandbits(1))]
|
|
84
|
+
self.assertEqual(self.client.write_multiple_coils(addr, bits_l), True)
|
|
85
|
+
self.assertEqual(self.client.read_coils(addr, len(bits_l)), bits_l)
|
|
86
|
+
# coils space: multiple read/write at max size
|
|
87
|
+
bits_l = [bool(getrandbits(1)) for _ in range(MAX_WRITABLE_BITS)]
|
|
88
|
+
self.assertEqual(self.client.write_multiple_coils(addr, bits_l), True)
|
|
89
|
+
self.assertEqual(self.client.read_coils(addr, len(bits_l)), bits_l)
|
|
90
|
+
# coils space: oversized multi-write
|
|
91
|
+
bits_l.append(bool(getrandbits(1)))
|
|
92
|
+
self.assertRaises(ValueError, self.client.write_multiple_coils, addr, bits_l)
|
|
93
|
+
# coils space: read/write over limit
|
|
94
|
+
self.assertRaises(ValueError, self.client.read_coils, 0xfffe, 3)
|
|
95
|
+
self.assertRaises(ValueError, self.client.write_single_coil, 0x10000, False)
|
|
96
|
+
self.assertRaises(ValueError, self.client.write_multiple_coils, 0xfff0, [False] * 17)
|
|
97
|
+
|
|
98
|
+
# discrete inputs
|
|
99
|
+
for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_READABLE_BITS]:
|
|
100
|
+
# discrete inputs space: single read/write
|
|
101
|
+
bit = bool(getrandbits(1))
|
|
102
|
+
self.server.data_bank.set_discrete_inputs(addr, [bit])
|
|
103
|
+
self.assertEqual(self.client.read_discrete_inputs(addr), [bit])
|
|
104
|
+
# discrete inputs space: multiple read/write at min size
|
|
105
|
+
bits_l = [bool(getrandbits(1))]
|
|
106
|
+
self.server.data_bank.set_discrete_inputs(addr, bits_l)
|
|
107
|
+
self.assertEqual(self.client.read_discrete_inputs(addr, len(bits_l)), bits_l)
|
|
108
|
+
# discrete inputs space: multiple read/write at max size
|
|
109
|
+
bits_l = [bool(getrandbits(1)) for _ in range(MAX_READABLE_BITS)]
|
|
110
|
+
self.server.data_bank.set_discrete_inputs(addr, bits_l)
|
|
111
|
+
self.assertEqual(self.client.read_discrete_inputs(addr, len(bits_l)), bits_l)
|
|
112
|
+
# discrete inputs space: multiple read/write at max size
|
|
113
|
+
bits_l.append(bool(getrandbits(1)))
|
|
114
|
+
self.server.data_bank.set_discrete_inputs(addr, bits_l)
|
|
115
|
+
self.assertRaises(ValueError, self.client.read_discrete_inputs, addr, len(bits_l))
|
|
116
|
+
# discrete inputs space: read/write over limit
|
|
117
|
+
self.assertRaises(ValueError, self.client.read_discrete_inputs, 0xffff, 2)
|
|
118
|
+
|
|
119
|
+
# holding registers
|
|
120
|
+
for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_WRITABLE_REGS]:
|
|
121
|
+
# holding registers space: single read/write
|
|
122
|
+
word = randint(0, 0xffff)
|
|
123
|
+
self.assertEqual(self.client.write_single_register(addr, word), True)
|
|
124
|
+
self.assertEqual(self.client.read_holding_registers(addr), [word])
|
|
125
|
+
# holding registers space: multi-write at max size
|
|
126
|
+
words_l = [randint(0, 0xffff) for _ in range(MAX_WRITABLE_REGS)]
|
|
127
|
+
self.assertEqual(self.client.write_multiple_registers(addr, words_l), True)
|
|
128
|
+
self.assertEqual(self.client.read_holding_registers(addr, len(words_l)), words_l)
|
|
129
|
+
# holding registers space: multi-write at max size
|
|
130
|
+
words_l = [randint(0, 0xffff) for _ in range(MAX_WRITE_READ_REGS)]
|
|
131
|
+
self.assertEqual(self.client.write_read_multiple_registers(addr, words_l, addr, len(words_l)), words_l)
|
|
132
|
+
self.assertEqual(self.client.read_holding_registers(addr, len(words_l)), words_l)
|
|
133
|
+
# holding registers space: read/write over limit
|
|
134
|
+
self.assertRaises(ValueError, self.client.read_holding_registers, 0xfff0, 17)
|
|
135
|
+
self.assertRaises(ValueError, self.client.write_single_register, 0, 0x10000)
|
|
136
|
+
self.assertRaises(ValueError, self.client.write_single_register, 0x10000, 0)
|
|
137
|
+
self.assertRaises(ValueError, self.client.write_multiple_registers, 0x1000, [0x10000])
|
|
138
|
+
self.assertRaises(ValueError, self.client.write_multiple_registers, 0xfff0, [0] * 17)
|
|
139
|
+
self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0x1000, [0x10000], 0x1000, 1)
|
|
140
|
+
self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0xfff0, [0] * 17, 0xfff0, 1)
|
|
141
|
+
self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0xfff0, [0] * 1, 0xfff0, 17)
|
|
142
|
+
|
|
143
|
+
# input registers
|
|
144
|
+
for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_READABLE_REGS]:
|
|
145
|
+
# input registers space: single read/write
|
|
146
|
+
word = randint(0, 0xffff)
|
|
147
|
+
self.server.data_bank.set_input_registers(addr, [word])
|
|
148
|
+
self.assertEqual(self.client.read_input_registers(addr), [word])
|
|
149
|
+
# input registers space: multiple read/write at max size
|
|
150
|
+
words_l = [randint(0, 0xffff) for _ in range(MAX_READABLE_REGS)]
|
|
151
|
+
self.server.data_bank.set_input_registers(addr, words_l)
|
|
152
|
+
self.assertEqual(self.client.read_input_registers(addr, len(words_l)), words_l)
|
|
153
|
+
# input registers space: multiple read/write over sized
|
|
154
|
+
words_l.append(randint(0, 0xffff))
|
|
155
|
+
self.server.data_bank.set_input_registers(addr, words_l)
|
|
156
|
+
self.assertRaises(ValueError, self.client.read_input_registers, addr, len(words_l))
|
|
157
|
+
# input registers space: read/write over limit
|
|
158
|
+
self.assertRaises(ValueError, self.client.read_input_registers, 0xfff0, 17)
|
|
159
|
+
|
|
160
|
+
def test_server_strength(self):
|
|
161
|
+
"""Test server responses to abnormal events."""
|
|
162
|
+
# unsupported function codes must return except EXP_ILLEGAL_FUNCTION
|
|
163
|
+
for func_code in range(0x80):
|
|
164
|
+
if func_code not in SUPPORTED_FUNCTION_CODES:
|
|
165
|
+
# test with a min PDU length of 2 bytes (avoid short frame error)
|
|
166
|
+
self.assertEqual(self.client.custom_request(bytes([func_code, 0x00])), None)
|
|
167
|
+
self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
|
|
168
|
+
self.assertEqual(self.client.last_except, EXP_ILLEGAL_FUNCTION)
|
|
169
|
+
# check a regular request status: no error, no except
|
|
170
|
+
self.assertEqual(self.client.read_coils(0), [False])
|
|
171
|
+
self.assertEqual(self.client.last_error, MB_NO_ERR)
|
|
172
|
+
self.assertEqual(self.client.last_except, EXP_NONE)
|
|
173
|
+
|
|
174
|
+
def test_server_read_identification(self):
|
|
175
|
+
"""Test server device indentification function."""
|
|
176
|
+
# forge a basic read identification on unconfigured server (return a data address except)
|
|
177
|
+
self.assertEqual(self.client.custom_request(b'\x2b\x0e\x01\x00'), None)
|
|
178
|
+
self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
|
|
179
|
+
self.assertEqual(self.client.last_except, EXP_DATA_ADDRESS)
|
|
180
|
+
# configure server
|
|
181
|
+
self.server.device_id = DeviceIdentification()
|
|
182
|
+
self.server.device_id.vendor_name = b'me'
|
|
183
|
+
self.server.device_id[0x80] = b'\xc0\xde'
|
|
184
|
+
# forge a basic read identification on a configured server (return a valid pdu)
|
|
185
|
+
self.assertNotEqual(self.client.custom_request(b'\x2b\x0e\x01\x00'), None)
|
|
186
|
+
# forge a read identifaction request with a bad read device id code (return except 3)
|
|
187
|
+
self.assertEqual(self.client.custom_request(b'\x2b\x0e\x05\x00'), None)
|
|
188
|
+
self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
|
|
189
|
+
self.assertEqual(self.client.last_except, EXP_DATA_VALUE)
|
|
190
|
+
# read VendorName str object(id #0) with individual access (read device id = 4)
|
|
191
|
+
ret_pdu = self.client.custom_request(b'\x2b\x0e\x04\x00')
|
|
192
|
+
self.assertEqual(ret_pdu, b'\x2b\x0e\x04\x83\x00\x00\x01\x00\x02me')
|
|
193
|
+
# read private int object(id #0x80) with individual access (read device id = 4)
|
|
194
|
+
ret_pdu = self.client.custom_request(b'\x2b\x0e\x04\x80')
|
|
195
|
+
self.assertEqual(ret_pdu, b'\x2b\x0e\x04\x83\x00\x00\x01\x80\x02\xc0\xde')
|
|
196
|
+
# restore default configuration
|
|
197
|
+
self.server.device_id = None
|
|
198
|
+
|
|
199
|
+
def test_client_read_identification(self):
|
|
200
|
+
"""Test client device indentification function."""
|
|
201
|
+
# configure server
|
|
202
|
+
vendor_name = ''.join(choice(ascii_letters) for _ in range(16)).encode()
|
|
203
|
+
product_code = ''.join(choice(ascii_letters) for _ in range(32)).encode()
|
|
204
|
+
maj_min_rev = b'v2.0'
|
|
205
|
+
vendor_url = b'https://github.com/sourceperl/pyModbusTCP'
|
|
206
|
+
self.server.device_id = DeviceIdentification(vendor_name=vendor_name, product_code=product_code,
|
|
207
|
+
major_minor_revision=maj_min_rev, vendor_url=vendor_url)
|
|
208
|
+
# read_device_identification: read basic device identification (stream access)
|
|
209
|
+
dev_id_resp = self.client.read_device_identification()
|
|
210
|
+
if not dev_id_resp:
|
|
211
|
+
self.fail('ModbusClient.read_device_identification() method failed unexpectedly')
|
|
212
|
+
else:
|
|
213
|
+
# return DeviceIdentificationResponse on success
|
|
214
|
+
self.assertEqual(isinstance(dev_id_resp, DeviceIdentificationResponse), True)
|
|
215
|
+
# check read data
|
|
216
|
+
self.assertEqual(len(dev_id_resp.objects_by_id), 3)
|
|
217
|
+
self.assertEqual(dev_id_resp.vendor_name, vendor_name)
|
|
218
|
+
self.assertEqual(dev_id_resp.objects_by_id.get(0), vendor_name)
|
|
219
|
+
self.assertEqual(dev_id_resp.product_code, product_code)
|
|
220
|
+
self.assertEqual(dev_id_resp.objects_by_id.get(1), product_code)
|
|
221
|
+
self.assertEqual(dev_id_resp.major_minor_revision, maj_min_rev)
|
|
222
|
+
self.assertEqual(dev_id_resp.objects_by_id.get(2), maj_min_rev)
|
|
223
|
+
self.assertEqual(dev_id_resp.vendor_url, None)
|
|
224
|
+
self.assertEqual(dev_id_resp.product_name, None)
|
|
225
|
+
self.assertEqual(dev_id_resp.model_name, None)
|
|
226
|
+
self.assertEqual(dev_id_resp.user_application_name, None)
|
|
227
|
+
# read_device_identification: read one specific identification object (individual access)
|
|
228
|
+
dev_id_resp = self.client.read_device_identification(read_code=4, object_id=3)
|
|
229
|
+
if not dev_id_resp:
|
|
230
|
+
self.fail('ModbusClient.read_device_identification() method failed unexpectedly')
|
|
231
|
+
else:
|
|
232
|
+
# return DeviceIdentificationResponse on success
|
|
233
|
+
self.assertEqual(isinstance(dev_id_resp, DeviceIdentificationResponse), True)
|
|
234
|
+
# check read data
|
|
235
|
+
self.assertEqual(len(dev_id_resp.objects_by_id), 1)
|
|
236
|
+
self.assertEqual(dev_id_resp.vendor_url, vendor_url)
|
|
237
|
+
self.assertEqual(dev_id_resp.objects_by_id.get(3), vendor_url)
|
|
238
|
+
# restore default configuration
|
|
239
|
+
self.server.device_id = None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == '__main__':
|
|
243
|
+
unittest.main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
""" Test of pyModbusTCP.ModbusServer """
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from pyModbusTCP.server import ModbusServer, DeviceIdentification
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestModbusServer(unittest.TestCase):
|
|
8
|
+
""" ModbusServer tests class. """
|
|
9
|
+
|
|
10
|
+
def test_device_identification(self):
|
|
11
|
+
"""Some tests around modbus device identification."""
|
|
12
|
+
# should raise exception
|
|
13
|
+
self.assertRaises(TypeError, ModbusServer, device_id=object())
|
|
14
|
+
# shouldn't raise exception
|
|
15
|
+
try:
|
|
16
|
+
ModbusServer(device_id=DeviceIdentification())
|
|
17
|
+
except Exception as e:
|
|
18
|
+
self.fail('ModbusServer raised exception "%r" unexpectedly' % e)
|
|
19
|
+
# init a DeviceIdentification class for test it
|
|
20
|
+
device_id = DeviceIdentification()
|
|
21
|
+
# should raise exception
|
|
22
|
+
with self.assertRaises(TypeError):
|
|
23
|
+
device_id['obj_name'] = 'anything'
|
|
24
|
+
with self.assertRaises(TypeError):
|
|
25
|
+
device_id[0] = 42
|
|
26
|
+
# shouldn't raise exception
|
|
27
|
+
try:
|
|
28
|
+
device_id.vendor_name = b'me'
|
|
29
|
+
device_id.user_application_name = b'unittest'
|
|
30
|
+
device_id[0x80] = b'feed'
|
|
31
|
+
except Exception as e:
|
|
32
|
+
self.fail('DeviceIdentification raised exception "%r" unexpectedly' % e)
|
|
33
|
+
# check access by shortcut name (str) or object id (int) return same value
|
|
34
|
+
self.assertEqual(device_id.vendor_name, device_id[0x00])
|
|
35
|
+
self.assertEqual(device_id.user_application_name, device_id[0x06])
|
|
36
|
+
# test __repr__
|
|
37
|
+
device_id = DeviceIdentification(
|
|
38
|
+
product_name=b'server', objects_id={42: b'this'})
|
|
39
|
+
self.assertEqual(repr(device_id), "DeviceIdentification(product_name=b'server', objects_id={42: b'this'})")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == '__main__':
|
|
43
|
+
unittest.main()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
""" Test of pyModbusTCP.utils """
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
import math
|
|
5
|
+
from pyModbusTCP.utils import \
|
|
6
|
+
get_bits_from_int, int2bits, decode_ieee, encode_ieee, \
|
|
7
|
+
word_list_to_long, words2longs, long_list_to_word, longs2words, \
|
|
8
|
+
get_2comp, twos_c, get_list_2comp, twos_c_l
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestUtils(unittest.TestCase):
|
|
12
|
+
""" pyModbusTCP.utils function test class. """
|
|
13
|
+
|
|
14
|
+
def test_get_bits_from_int(self):
|
|
15
|
+
"""Test function get_bits_from_int and it's short alias int2bits."""
|
|
16
|
+
# default bits list size is 16
|
|
17
|
+
self.assertEqual(len(get_bits_from_int(0)), 16)
|
|
18
|
+
# for 8 size (positional arg)
|
|
19
|
+
self.assertEqual(len(get_bits_from_int(0, 8)), 8)
|
|
20
|
+
# for 32 size (named arg)
|
|
21
|
+
self.assertEqual(len(get_bits_from_int(0, val_size=32)), 32)
|
|
22
|
+
# test binary decode
|
|
23
|
+
self.assertEqual(int2bits(0x0000), [False]*16)
|
|
24
|
+
self.assertEqual(int2bits(0xffff), [True]*16)
|
|
25
|
+
self.assertEqual(int2bits(0xf007), [True]*3 + [False]*9 + [True]*4)
|
|
26
|
+
self.assertEqual(int2bits(6, 4), [False, True, True, False])
|
|
27
|
+
|
|
28
|
+
def test_ieee(self):
|
|
29
|
+
"""Test IEEE functions: decode_ieee and encode_ieee."""
|
|
30
|
+
# test IEEE NaN
|
|
31
|
+
self.assertTrue(math.isnan(decode_ieee(0x7fc00000)))
|
|
32
|
+
self.assertEqual(encode_ieee(float('nan')), 0x7fc00000)
|
|
33
|
+
# test +/- infinity
|
|
34
|
+
self.assertTrue(math.isinf(decode_ieee(0xff800000)))
|
|
35
|
+
self.assertTrue(math.isinf(decode_ieee(0x7f800000)))
|
|
36
|
+
# test big and small values
|
|
37
|
+
avogad = 6.022140857e+23
|
|
38
|
+
avo_32 = 0x66ff0c2f
|
|
39
|
+
avo_64 = 0x44dfe185d2f54b67
|
|
40
|
+
planck = 6.62606957e-34
|
|
41
|
+
pla_32 = 0x085c305e
|
|
42
|
+
pla_64 = 0x390b860bb596a559
|
|
43
|
+
# IEEE single or double precision format -> float
|
|
44
|
+
self.assertAlmostEqual(decode_ieee(avo_32), avogad, delta=avogad*1e-7)
|
|
45
|
+
self.assertAlmostEqual(decode_ieee(avo_64, double=True), avogad)
|
|
46
|
+
self.assertAlmostEqual(decode_ieee(pla_32), planck)
|
|
47
|
+
self.assertAlmostEqual(decode_ieee(pla_64, double=True), planck)
|
|
48
|
+
# float -> IEEE single or double precision format
|
|
49
|
+
self.assertAlmostEqual(encode_ieee(avogad), avo_32)
|
|
50
|
+
self.assertAlmostEqual(encode_ieee(avogad, double=True), avo_64)
|
|
51
|
+
self.assertAlmostEqual(encode_ieee(planck), pla_32)
|
|
52
|
+
self.assertAlmostEqual(encode_ieee(planck, double=True), pla_64)
|
|
53
|
+
|
|
54
|
+
def test_word_list_to_long(self):
|
|
55
|
+
"""Test function word_list_to_long and it 's short alias words2longs."""
|
|
56
|
+
# empty list, return empty list
|
|
57
|
+
self.assertEqual(word_list_to_long([]), [])
|
|
58
|
+
# if len of list is odd ignore last value
|
|
59
|
+
self.assertEqual(word_list_to_long([0x1, 0x2, 0x3]), [0x10002])
|
|
60
|
+
# test convert with big and little endian
|
|
61
|
+
l1 = [0xdead, 0xbeef]
|
|
62
|
+
l2 = [0xfeed, 0xface, 0xcafe, 0xbeef]
|
|
63
|
+
big = dict(big_endian=True)
|
|
64
|
+
nobig = dict(big_endian=False)
|
|
65
|
+
big64 = dict(big_endian=True, long_long=True)
|
|
66
|
+
nobig64 = dict(big_endian=False, long_long=True)
|
|
67
|
+
self.assertEqual(words2longs(l1, **big), [0xdeadbeef])
|
|
68
|
+
self.assertEqual(words2longs(l2, **big), [0xfeedface, 0xcafebeef])
|
|
69
|
+
self.assertEqual(words2longs(l1, **nobig), [0xbeefdead])
|
|
70
|
+
self.assertEqual(words2longs(l2, **nobig), [0xfacefeed, 0xbeefcafe])
|
|
71
|
+
self.assertEqual(words2longs(l1*2, **big64), [0xdeadbeefdeadbeef])
|
|
72
|
+
self.assertEqual(words2longs(l2*2, **big64), [0xfeedfacecafebeef]*2)
|
|
73
|
+
self.assertEqual(words2longs(l1*2, **nobig64), [0xbeefdeadbeefdead])
|
|
74
|
+
self.assertEqual(words2longs(l2*2, **nobig64), [0xbeefcafefacefeed]*2)
|
|
75
|
+
|
|
76
|
+
def test_long_list_to_word(self):
|
|
77
|
+
"""Test function long_list_to_word and short alias longs2words."""
|
|
78
|
+
# empty list, return empty list
|
|
79
|
+
self.assertEqual(long_list_to_word([]), [])
|
|
80
|
+
# test convert with big and little endian
|
|
81
|
+
l1 = [0xdeadbeef]
|
|
82
|
+
l1_big = [0xdead, 0xbeef]
|
|
83
|
+
l1_nobig = [0xbeef, 0xdead]
|
|
84
|
+
l1_big64 = [0x0000, 0x0000, 0xdead, 0xbeef]
|
|
85
|
+
l1_nobig64 = [0xbeef, 0xdead, 0x0000, 0x0000]
|
|
86
|
+
l2 = [0xfeedface, 0xcafebeef]
|
|
87
|
+
l2_big = [0xfeed, 0xface, 0xcafe, 0xbeef]
|
|
88
|
+
l2_nobig = [0xface, 0xfeed, 0xbeef, 0xcafe]
|
|
89
|
+
l3 = [0xfeedfacecafebeef]
|
|
90
|
+
l3_big64 = [0xfeed, 0xface, 0xcafe, 0xbeef]
|
|
91
|
+
l3_nobig64 = [0xbeef, 0xcafe, 0xface, 0xfeed]
|
|
92
|
+
big = dict(big_endian=True)
|
|
93
|
+
nobig = dict(big_endian=False)
|
|
94
|
+
big64 = dict(big_endian=True, long_long=True)
|
|
95
|
+
nobig64 = dict(big_endian=False, long_long=True)
|
|
96
|
+
self.assertEqual(longs2words(l1, **big), l1_big)
|
|
97
|
+
self.assertEqual(longs2words(l2, **big), l2_big)
|
|
98
|
+
self.assertEqual(longs2words(l1, **nobig), l1_nobig)
|
|
99
|
+
self.assertEqual(longs2words(l2, **nobig), l2_nobig)
|
|
100
|
+
self.assertEqual(longs2words(l1*2, **big64), l1_big64*2)
|
|
101
|
+
self.assertEqual(longs2words(l3*2, **big64), l3_big64*2)
|
|
102
|
+
self.assertEqual(longs2words(l1*4, **nobig64), l1_nobig64*4)
|
|
103
|
+
self.assertEqual(longs2words(l3*4, **nobig64), l3_nobig64*4)
|
|
104
|
+
|
|
105
|
+
def test_get_2comp(self):
|
|
106
|
+
"""Test function get_2comp and it's short alias twos_c."""
|
|
107
|
+
# check if ValueError exception is raised
|
|
108
|
+
self.assertRaises(ValueError, get_2comp, 0x10000)
|
|
109
|
+
self.assertRaises(ValueError, get_2comp, -0x8001)
|
|
110
|
+
self.assertRaises(ValueError, twos_c, 0x100000000, val_size=32)
|
|
111
|
+
self.assertRaises(ValueError, twos_c, -0x80000001, val_size=32)
|
|
112
|
+
# 2's complement of 16bits values (default)
|
|
113
|
+
self.assertEqual(get_2comp(0x0001), 0x0001)
|
|
114
|
+
self.assertEqual(get_2comp(0x8000), -0x8000)
|
|
115
|
+
self.assertEqual(get_2comp(-0x8000), 0x8000)
|
|
116
|
+
self.assertEqual(get_2comp(0xffff), -0x0001)
|
|
117
|
+
self.assertEqual(get_2comp(-0x0001), 0xffff)
|
|
118
|
+
self.assertEqual(get_2comp(-0x00fa), 0xff06)
|
|
119
|
+
self.assertEqual(get_2comp(0xff06), -0x00fa)
|
|
120
|
+
# 2's complement of 32bits values
|
|
121
|
+
self.assertEqual(twos_c(0xfffffff, val_size=32), 0xfffffff)
|
|
122
|
+
self.assertEqual(twos_c(-1, val_size=32), 0xffffffff)
|
|
123
|
+
self.assertEqual(twos_c(0xffffffff, val_size=32), -1)
|
|
124
|
+
self.assertEqual(twos_c(125, val_size=32), 0x0000007d)
|
|
125
|
+
self.assertEqual(twos_c(0x0000007d, val_size=32), 125)
|
|
126
|
+
self.assertEqual(twos_c(-250, val_size=32), 0xffffff06)
|
|
127
|
+
self.assertEqual(twos_c(0xffffff06, val_size=32), -250)
|
|
128
|
+
self.assertEqual(twos_c(0xfffea2a5, val_size=32), -89435)
|
|
129
|
+
self.assertEqual(twos_c(-89435, val_size=32), 0xfffea2a5)
|
|
130
|
+
|
|
131
|
+
def test_get_list_2comp(self):
|
|
132
|
+
"""Test get_list_2comp and it's short alias twos_c_l."""
|
|
133
|
+
self.assertEqual(get_list_2comp([0x8000], 16), [-32768])
|
|
134
|
+
in_l = [0x8000, 0xffff, 0x0042]
|
|
135
|
+
out_l = [-0x8000, -0x0001, 0x42]
|
|
136
|
+
self.assertEqual(twos_c_l(in_l, val_size=16), out_l)
|
|
137
|
+
in_l = [0x8000, 0xffffffff, 0xfffea2a5]
|
|
138
|
+
out_l = [0x8000, -0x0001, -89435]
|
|
139
|
+
self.assertEqual(twos_c_l(in_l, val_size=32), out_l)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == '__main__':
|
|
143
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|