pyModbusTCP 0.2.2__tar.gz → 0.3.0__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.
@@ -1,5 +1,10 @@
1
1
  Revision history for pyModbusTCP
2
2
 
3
+ 0.3.0 2024-09-04
4
+
5
+ - pyModbusTCP.client: now use the standard logging method as in the server part.
6
+ - ModbusClient: debug flag is removed (see examples/client_debug.py).
7
+
3
8
  0.2.2 2024-07-31
4
9
 
5
10
  - fix ModbusServer: wrong check of discrete inputs length in DataBank (thanks to OTnetproj).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyModbusTCP
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: A simple Modbus/TCP library for Python
5
5
  Home-page: https://github.com/sourceperl/pyModbusTCP
6
6
  Author: Loic Lefebvre
@@ -6,12 +6,14 @@
6
6
  # 1,2,4,5,6 (Class 1)
7
7
  # 15,23,43
8
8
 
9
- from .constants import VERSION
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
- from dataclasses import dataclass, field
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, debug=%s, auto_open=%s, auto_close=%s)'
126
- r_str %= (self.host, self.port, self.unit_id, self.timeout, self.debug, self.auto_open, self.auto_close)
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, MEI_TYPE_READ_DEVICE_ID, read_code, object_id)
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()
@@ -800,7 +797,7 @@ class ModbusClient:
800
797
  # send frame with error check
801
798
  self._send(tx_frame)
802
799
  # debug
803
- self._debug_dump('Tx', tx_frame)
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._debug_dump('Rx', rx_mbap)
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._debug_dump('Rx', rx_mbap + rx_pdu)
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 %d "%s")' % (self.last_except, self.last_except_as_txt))
933
-
934
- def _debug_msg(self, msg):
935
- """Print debug message if debug mode is on.
936
-
937
- :param msg: debug message
938
- :type msg: str
939
- """
940
- if self.debug:
941
- print(msg)
942
-
943
- def _debug_dump(self, label, frame):
944
- """Print debug dump if debug mode is on.
945
-
946
- :param label: head label
947
- :type label: str
948
- :param frame: modbus frame
949
- :type frame: bytes
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,7 +1,7 @@
1
1
  """ pyModbusTCP package constants definition """
2
2
 
3
3
  # Package version
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.0'
5
5
  # Modbus/TCP
6
6
  MODBUS_PORT = 502
7
7
  # Modbus function code
@@ -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 threading import Lock, Thread, Event
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('pyModbusTCP.server')
21
+ logger = logging.getLogger(__name__)
17
22
 
18
23
 
19
24
  class DataBank:
@@ -825,7 +830,7 @@ class ModbusServer:
825
830
  session_data = ModbusServer.SessionData()
826
831
  (session_data.client.address, session_data.client.port) = self.request.getpeername()
827
832
  # debug message
828
- logger.debug('Accept new connection from %r', session_data.client)
833
+ logger.debug('accept new connection from %r', session_data.client)
829
834
  try:
830
835
  # main processing loop
831
836
  while True:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyModbusTCP
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: A simple Modbus/TCP library for Python
5
5
  Home-page: https://github.com/sourceperl/pyModbusTCP
6
6
  Author: Loic Lefebvre
@@ -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,214 @@
1
+ """ Test of pyModbusTCP client-server interaction """
2
+
3
+ import unittest
4
+ from random import randint, getrandbits, choice
5
+ from string import ascii_letters
6
+ from pyModbusTCP.server import ModbusServer, DeviceIdentification
7
+ from pyModbusTCP.client import ModbusClient, DeviceIdentificationResponse
8
+ from pyModbusTCP.constants import SUPPORTED_FUNCTION_CODES, \
9
+ EXP_NONE, EXP_ILLEGAL_FUNCTION, EXP_DATA_ADDRESS, EXP_DATA_VALUE, MB_NO_ERR, MB_EXCEPT_ERR
10
+
11
+
12
+ # some const
13
+ MAX_READABLE_REGS = 125
14
+ MAX_WRITABLE_REGS = 123
15
+ MAX_WRITE_READ_REGS = 121
16
+ MAX_READABLE_BITS = 2000
17
+ MAX_WRITABLE_BITS = 1968
18
+
19
+
20
+ class TestClientServer(unittest.TestCase):
21
+ """ Client-server interaction test class. """
22
+
23
+ def setUp(self):
24
+ """Init client-server for test_xxx methods."""
25
+ # modbus server
26
+ self.server = ModbusServer(port=5020, no_block=True)
27
+ self.server.start()
28
+ # modbus client
29
+ self.client = ModbusClient(port=5020)
30
+ self.client.open()
31
+
32
+ def tearDown(self):
33
+ """Cleanning after test."""
34
+ self.client.close()
35
+ self.server.stop()
36
+
37
+ def test_default_startup_values(self):
38
+ """Some read at random address to test startup values."""
39
+ for addr in [randint(0, 0xffff) for _ in range(100)]:
40
+ self.assertEqual(self.client.read_coils(addr), [False])
41
+ self.assertEqual(self.client.read_discrete_inputs(addr), [False])
42
+ self.assertEqual(self.client.read_holding_registers(addr), [0])
43
+ self.assertEqual(self.client.read_input_registers(addr), [0])
44
+
45
+ def test_read_write_requests(self):
46
+ """Test standard modbus functions."""
47
+ # coils
48
+ for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_WRITABLE_BITS]:
49
+ # coils space: single read/write
50
+ bit = bool(getrandbits(1))
51
+ self.assertEqual(self.client.write_single_coil(addr, bit), True)
52
+ self.assertEqual(self.client.read_coils(addr), [bit])
53
+ # coils space: multiple read/write at min size
54
+ bits_l = [bool(getrandbits(1))]
55
+ self.assertEqual(self.client.write_multiple_coils(addr, bits_l), True)
56
+ self.assertEqual(self.client.read_coils(addr, len(bits_l)), bits_l)
57
+ # coils space: multiple read/write at max size
58
+ bits_l = [bool(getrandbits(1)) for _ in range(MAX_WRITABLE_BITS)]
59
+ self.assertEqual(self.client.write_multiple_coils(addr, bits_l), True)
60
+ self.assertEqual(self.client.read_coils(addr, len(bits_l)), bits_l)
61
+ # coils space: oversized multi-write
62
+ bits_l.append(bool(getrandbits(1)))
63
+ self.assertRaises(ValueError, self.client.write_multiple_coils, addr, bits_l)
64
+ # coils space: read/write over limit
65
+ self.assertRaises(ValueError, self.client.read_coils, 0xfffe, 3)
66
+ self.assertRaises(ValueError, self.client.write_single_coil, 0x10000, False)
67
+ self.assertRaises(ValueError, self.client.write_multiple_coils, 0xfff0, [False] * 17)
68
+
69
+ # discrete inputs
70
+ for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_READABLE_BITS]:
71
+ # discrete inputs space: single read/write
72
+ bit = bool(getrandbits(1))
73
+ self.server.data_bank.set_discrete_inputs(addr, [bit])
74
+ self.assertEqual(self.client.read_discrete_inputs(addr), [bit])
75
+ # discrete inputs space: multiple read/write at min size
76
+ bits_l = [bool(getrandbits(1))]
77
+ self.server.data_bank.set_discrete_inputs(addr, bits_l)
78
+ self.assertEqual(self.client.read_discrete_inputs(addr, len(bits_l)), bits_l)
79
+ # discrete inputs space: multiple read/write at max size
80
+ bits_l = [bool(getrandbits(1)) for _ in range(MAX_READABLE_BITS)]
81
+ self.server.data_bank.set_discrete_inputs(addr, bits_l)
82
+ self.assertEqual(self.client.read_discrete_inputs(addr, len(bits_l)), bits_l)
83
+ # discrete inputs space: multiple read/write at max size
84
+ bits_l.append(bool(getrandbits(1)))
85
+ self.server.data_bank.set_discrete_inputs(addr, bits_l)
86
+ self.assertRaises(ValueError, self.client.read_discrete_inputs, addr, len(bits_l))
87
+ # discrete inputs space: read/write over limit
88
+ self.assertRaises(ValueError, self.client.read_discrete_inputs, 0xffff, 2)
89
+
90
+ # holding registers
91
+ for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_WRITABLE_REGS]:
92
+ # holding registers space: single read/write
93
+ word = randint(0, 0xffff)
94
+ self.assertEqual(self.client.write_single_register(addr, word), True)
95
+ self.assertEqual(self.client.read_holding_registers(addr), [word])
96
+ # holding registers space: multi-write at max size
97
+ words_l = [randint(0, 0xffff) for _ in range(MAX_WRITABLE_REGS)]
98
+ self.assertEqual(self.client.write_multiple_registers(addr, words_l), True)
99
+ self.assertEqual(self.client.read_holding_registers(addr, len(words_l)), words_l)
100
+ # holding registers space: multi-write at max size
101
+ words_l = [randint(0, 0xffff) for _ in range(MAX_WRITE_READ_REGS)]
102
+ self.assertEqual(self.client.write_read_multiple_registers(addr, words_l, addr, len(words_l)), words_l)
103
+ self.assertEqual(self.client.read_holding_registers(addr, len(words_l)), words_l)
104
+ # holding registers space: read/write over limit
105
+ self.assertRaises(ValueError, self.client.read_holding_registers, 0xfff0, 17)
106
+ self.assertRaises(ValueError, self.client.write_single_register, 0, 0x10000)
107
+ self.assertRaises(ValueError, self.client.write_single_register, 0x10000, 0)
108
+ self.assertRaises(ValueError, self.client.write_multiple_registers, 0x1000, [0x10000])
109
+ self.assertRaises(ValueError, self.client.write_multiple_registers, 0xfff0, [0] * 17)
110
+ self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0x1000, [0x10000], 0x1000, 1)
111
+ self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0xfff0, [0] * 17, 0xfff0, 1)
112
+ self.assertRaises(ValueError, self.client.write_read_multiple_registers, 0xfff0, [0] * 1, 0xfff0, 17)
113
+
114
+ # input registers
115
+ for addr in [0x0000, 0x1234, 0x2345, 0x10000 - MAX_READABLE_REGS]:
116
+ # input registers space: single read/write
117
+ word = randint(0, 0xffff)
118
+ self.server.data_bank.set_input_registers(addr, [word])
119
+ self.assertEqual(self.client.read_input_registers(addr), [word])
120
+ # input registers space: multiple read/write at max size
121
+ words_l = [randint(0, 0xffff) for _ in range(MAX_READABLE_REGS)]
122
+ self.server.data_bank.set_input_registers(addr, words_l)
123
+ self.assertEqual(self.client.read_input_registers(addr, len(words_l)), words_l)
124
+ # input registers space: multiple read/write over sized
125
+ words_l.append(randint(0, 0xffff))
126
+ self.server.data_bank.set_input_registers(addr, words_l)
127
+ self.assertRaises(ValueError, self.client.read_input_registers, addr, len(words_l))
128
+ # input registers space: read/write over limit
129
+ self.assertRaises(ValueError, self.client.read_input_registers, 0xfff0, 17)
130
+
131
+ def test_server_strength(self):
132
+ """Test server responses to abnormal events."""
133
+ # unsupported function codes must return except EXP_ILLEGAL_FUNCTION
134
+ for func_code in range(0x80):
135
+ if func_code not in SUPPORTED_FUNCTION_CODES:
136
+ # test with a min PDU length of 2 bytes (avoid short frame error)
137
+ self.assertEqual(self.client.custom_request(bytes([func_code, 0x00])), None)
138
+ self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
139
+ self.assertEqual(self.client.last_except, EXP_ILLEGAL_FUNCTION)
140
+ # check a regular request status: no error, no except
141
+ self.assertEqual(self.client.read_coils(0), [False])
142
+ self.assertEqual(self.client.last_error, MB_NO_ERR)
143
+ self.assertEqual(self.client.last_except, EXP_NONE)
144
+
145
+ def test_server_read_identification(self):
146
+ """Test server device indentification function."""
147
+ # forge a basic read identification on unconfigured server (return a data address except)
148
+ self.assertEqual(self.client.custom_request(b'\x2b\x0e\x01\x00'), None)
149
+ self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
150
+ self.assertEqual(self.client.last_except, EXP_DATA_ADDRESS)
151
+ # configure server
152
+ self.server.device_id = DeviceIdentification()
153
+ self.server.device_id.vendor_name = b'me'
154
+ self.server.device_id[0x80] = b'\xc0\xde'
155
+ # forge a basic read identification on a configured server (return a valid pdu)
156
+ self.assertNotEqual(self.client.custom_request(b'\x2b\x0e\x01\x00'), None)
157
+ # forge a read identifaction request with a bad read device id code (return except 3)
158
+ self.assertEqual(self.client.custom_request(b'\x2b\x0e\x05\x00'), None)
159
+ self.assertEqual(self.client.last_error, MB_EXCEPT_ERR)
160
+ self.assertEqual(self.client.last_except, EXP_DATA_VALUE)
161
+ # read VendorName str object(id #0) with individual access (read device id = 4)
162
+ ret_pdu = self.client.custom_request(b'\x2b\x0e\x04\x00')
163
+ self.assertEqual(ret_pdu, b'\x2b\x0e\x04\x83\x00\x00\x01\x00\x02me')
164
+ # read private int object(id #0x80) with individual access (read device id = 4)
165
+ ret_pdu = self.client.custom_request(b'\x2b\x0e\x04\x80')
166
+ self.assertEqual(ret_pdu, b'\x2b\x0e\x04\x83\x00\x00\x01\x80\x02\xc0\xde')
167
+ # restore default configuration
168
+ self.server.device_id = None
169
+
170
+ def test_client_read_identification(self):
171
+ """Test client device indentification function."""
172
+ # configure server
173
+ vendor_name = ''.join(choice(ascii_letters) for _ in range(16)).encode()
174
+ product_code = ''.join(choice(ascii_letters) for _ in range(32)).encode()
175
+ maj_min_rev = b'v2.0'
176
+ vendor_url = b'https://github.com/sourceperl/pyModbusTCP'
177
+ self.server.device_id = DeviceIdentification(vendor_name=vendor_name, product_code=product_code,
178
+ major_minor_revision=maj_min_rev, vendor_url=vendor_url)
179
+ # read_device_identification: read basic device identification (stream access)
180
+ dev_id_resp = self.client.read_device_identification()
181
+ if not dev_id_resp:
182
+ self.fail('ModbusClient.read_device_identification() method failed unexpectedly')
183
+ else:
184
+ # return DeviceIdentificationResponse on success
185
+ self.assertEqual(isinstance(dev_id_resp, DeviceIdentificationResponse), True)
186
+ # check read data
187
+ self.assertEqual(len(dev_id_resp.objects_by_id), 3)
188
+ self.assertEqual(dev_id_resp.vendor_name, vendor_name)
189
+ self.assertEqual(dev_id_resp.objects_by_id.get(0), vendor_name)
190
+ self.assertEqual(dev_id_resp.product_code, product_code)
191
+ self.assertEqual(dev_id_resp.objects_by_id.get(1), product_code)
192
+ self.assertEqual(dev_id_resp.major_minor_revision, maj_min_rev)
193
+ self.assertEqual(dev_id_resp.objects_by_id.get(2), maj_min_rev)
194
+ self.assertEqual(dev_id_resp.vendor_url, None)
195
+ self.assertEqual(dev_id_resp.product_name, None)
196
+ self.assertEqual(dev_id_resp.model_name, None)
197
+ self.assertEqual(dev_id_resp.user_application_name, None)
198
+ # read_device_identification: read one specific identification object (individual access)
199
+ dev_id_resp = self.client.read_device_identification(read_code=4, object_id=3)
200
+ if not dev_id_resp:
201
+ self.fail('ModbusClient.read_device_identification() method failed unexpectedly')
202
+ else:
203
+ # return DeviceIdentificationResponse on success
204
+ self.assertEqual(isinstance(dev_id_resp, DeviceIdentificationResponse), True)
205
+ # check read data
206
+ self.assertEqual(len(dev_id_resp.objects_by_id), 1)
207
+ self.assertEqual(dev_id_resp.vendor_url, vendor_url)
208
+ self.assertEqual(dev_id_resp.objects_by_id.get(3), vendor_url)
209
+ # restore default configuration
210
+ self.server.device_id = None
211
+
212
+
213
+ if __name__ == '__main__':
214
+ 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