pyModbusTCP 0.2.1__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.
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/CHANGES +10 -0
- pyModbusTCP-0.3.0/PKG-INFO +120 -0
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/README.rst +2 -2
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/pyModbusTCP/__init__.py +4 -2
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/pyModbusTCP/client.py +46 -73
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/pyModbusTCP/constants.py +1 -1
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/pyModbusTCP/server.py +16 -11
- pyModbusTCP-0.3.0/pyModbusTCP.egg-info/PKG-INFO +120 -0
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/pyModbusTCP.egg-info/SOURCES.txt +5 -1
- pyModbusTCP-0.3.0/tests/test_client.py +58 -0
- pyModbusTCP-0.3.0/tests/test_client_server.py +214 -0
- pyModbusTCP-0.3.0/tests/test_server.py +43 -0
- pyModbusTCP-0.3.0/tests/test_utils.py +143 -0
- pyModbusTCP-0.2.1/PKG-INFO +0 -119
- pyModbusTCP-0.2.1/pyModbusTCP.egg-info/PKG-INFO +0 -119
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/LICENSE +0 -0
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/MANIFEST.in +0 -0
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/pyModbusTCP/utils.py +0 -0
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/pyModbusTCP.egg-info/dependency_links.txt +0 -0
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/pyModbusTCP.egg-info/top_level.txt +0 -0
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/setup.cfg +0 -0
- {pyModbusTCP-0.2.1 → pyModbusTCP-0.3.0}/setup.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
8
|
+
0.2.2 2024-07-31
|
|
9
|
+
|
|
10
|
+
- fix ModbusServer: wrong check of discrete inputs length in DataBank (thanks to OTnetproj).
|
|
11
|
+
- updated compatibility test (python versions): remove 3.7, add 3.12.
|
|
12
|
+
|
|
3
13
|
0.2.1 2023-11-21
|
|
4
14
|
|
|
5
15
|
- fix ModbusServer: wrong check of input registers length in DataBank (thanks to monsieurvor).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyModbusTCP
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A simple Modbus/TCP library for Python
|
|
5
|
+
Home-page: https://github.com/sourceperl/pyModbusTCP
|
|
6
|
+
Author: Loic Lefebvre
|
|
7
|
+
Author-email: loic.celine@free.fr
|
|
8
|
+
License: MIT
|
|
9
|
+
Platform: any
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
|
|
12
|
+
.. |badge_tests| image:: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master
|
|
13
|
+
:target: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml
|
|
14
|
+
|
|
15
|
+
.. |badge_docs| image:: https://readthedocs.org/projects/pymodbustcp/badge/?version=latest
|
|
16
|
+
:target: http://pymodbustcp.readthedocs.io/
|
|
17
|
+
|
|
18
|
+
pyModbusTCP |badge_tests| |badge_docs|
|
|
19
|
+
======================================
|
|
20
|
+
|
|
21
|
+
A simple Modbus/TCP client library for Python.
|
|
22
|
+
pyModbusTCP is pure Python code without any extension or external module dependency.
|
|
23
|
+
|
|
24
|
+
Since version 0.1.0, a server is also available.
|
|
25
|
+
|
|
26
|
+
Tests
|
|
27
|
+
-----
|
|
28
|
+
|
|
29
|
+
The module is currently test on Python 3.8, 3.9, 3.10, 3.11 and 3.12.
|
|
30
|
+
|
|
31
|
+
For Linux, Mac OS and Windows.
|
|
32
|
+
|
|
33
|
+
Documentation
|
|
34
|
+
-------------
|
|
35
|
+
|
|
36
|
+
Documentation of the last release is available online at https://pymodbustcp.readthedocs.io/.
|
|
37
|
+
|
|
38
|
+
Setup
|
|
39
|
+
-----
|
|
40
|
+
|
|
41
|
+
You can install this package from:
|
|
42
|
+
|
|
43
|
+
PyPI, the easy way:
|
|
44
|
+
|
|
45
|
+
.. code-block:: bash
|
|
46
|
+
|
|
47
|
+
# install the last available release (stable)
|
|
48
|
+
sudo pip install pyModbusTCP
|
|
49
|
+
|
|
50
|
+
.. code-block:: bash
|
|
51
|
+
|
|
52
|
+
# install a specific version (here release v0.1.10)
|
|
53
|
+
sudo pip install pyModbusTCP==v0.1.10
|
|
54
|
+
|
|
55
|
+
From GitHub:
|
|
56
|
+
|
|
57
|
+
.. code-block:: bash
|
|
58
|
+
|
|
59
|
+
# install a specific version (here release v0.1.10) directly from github servers
|
|
60
|
+
sudo pip install git+https://github.com/sourceperl/pyModbusTCP.git@v0.1.10
|
|
61
|
+
|
|
62
|
+
Note on the use of versions:
|
|
63
|
+
|
|
64
|
+
Over time, some things can change. So, it's a good practice that you always use a specific version of a package for
|
|
65
|
+
your project, instead of just relying on the default behavior. Without precision, the installation tools will always
|
|
66
|
+
install the latest version available for a package, this may have some drawbacks. For example, in pyModbusTCP, the TCP
|
|
67
|
+
automatic open mode will be active by default from version 0.2.0. It is not the case with previous versions and it just
|
|
68
|
+
doesn't exist before the 0.0.12. This can lead to some strange behaviour of your application if you are not aware of
|
|
69
|
+
the change. Look at `CHANGES <https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES>`_ for details on versions
|
|
70
|
+
available.
|
|
71
|
+
|
|
72
|
+
Usage example
|
|
73
|
+
-------------
|
|
74
|
+
|
|
75
|
+
See examples/ for full scripts.
|
|
76
|
+
|
|
77
|
+
include (for all samples)
|
|
78
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
79
|
+
|
|
80
|
+
.. code-block:: python
|
|
81
|
+
|
|
82
|
+
from pyModbusTCP.client import ModbusClient
|
|
83
|
+
|
|
84
|
+
module init (TCP always open)
|
|
85
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
86
|
+
|
|
87
|
+
.. code-block:: python
|
|
88
|
+
|
|
89
|
+
# TCP auto connect on first modbus request
|
|
90
|
+
c = ModbusClient(host="localhost", port=502, unit_id=1, auto_open=True)
|
|
91
|
+
|
|
92
|
+
module init (TCP open/close for each request)
|
|
93
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
94
|
+
|
|
95
|
+
.. code-block:: python
|
|
96
|
+
|
|
97
|
+
# TCP auto connect on modbus request, close after it
|
|
98
|
+
c = ModbusClient(host="127.0.0.1", auto_open=True, auto_close=True)
|
|
99
|
+
|
|
100
|
+
Read 2x 16 bits registers at modbus address 0 :
|
|
101
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
102
|
+
|
|
103
|
+
.. code-block:: python
|
|
104
|
+
|
|
105
|
+
regs = c.read_holding_registers(0, 2)
|
|
106
|
+
|
|
107
|
+
if regs:
|
|
108
|
+
print(regs)
|
|
109
|
+
else:
|
|
110
|
+
print("read error")
|
|
111
|
+
|
|
112
|
+
Write value 44 and 55 to registers at modbus address 10 :
|
|
113
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
114
|
+
|
|
115
|
+
.. code-block:: python
|
|
116
|
+
|
|
117
|
+
if c.write_multiple_registers(10, [44,55]):
|
|
118
|
+
print("write ok")
|
|
119
|
+
else:
|
|
120
|
+
print("write error")
|
|
@@ -10,12 +10,12 @@ pyModbusTCP |badge_tests| |badge_docs|
|
|
|
10
10
|
A simple Modbus/TCP client library for Python.
|
|
11
11
|
pyModbusTCP is pure Python code without any extension or external module dependency.
|
|
12
12
|
|
|
13
|
-
Since version 0.1.0, a server is also available
|
|
13
|
+
Since version 0.1.0, a server is also available.
|
|
14
14
|
|
|
15
15
|
Tests
|
|
16
16
|
-----
|
|
17
17
|
|
|
18
|
-
The module is currently test on Python 3.
|
|
18
|
+
The module is currently test on Python 3.8, 3.9, 3.10, 3.11 and 3.12.
|
|
19
19
|
|
|
20
20
|
For Linux, Mac OS and Windows.
|
|
21
21
|
|
|
@@ -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()
|
|
@@ -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:
|
|
@@ -171,7 +176,7 @@ class DataBank:
|
|
|
171
176
|
"""
|
|
172
177
|
# secure extract of data from list used by server thread
|
|
173
178
|
with self._d_inputs_lock:
|
|
174
|
-
if (address >= 0) and (address + number <= len(self.
|
|
179
|
+
if (address >= 0) and (address + number <= len(self._d_inputs)):
|
|
175
180
|
return self._d_inputs[address: number + address]
|
|
176
181
|
else:
|
|
177
182
|
return None
|
|
@@ -191,7 +196,7 @@ class DataBank:
|
|
|
191
196
|
bit_list = [bool(b) for b in bit_list]
|
|
192
197
|
# ensure atomic update of internal data
|
|
193
198
|
with self._d_inputs_lock:
|
|
194
|
-
if (address >= 0) and (address + len(bit_list) <= len(self.
|
|
199
|
+
if (address >= 0) and (address + len(bit_list) <= len(self._d_inputs)):
|
|
195
200
|
for offset, b_value in enumerate(bit_list):
|
|
196
201
|
self._d_inputs[address + offset] = b_value
|
|
197
202
|
else:
|
|
@@ -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('
|
|
833
|
+
logger.debug('accept new connection from %r', session_data.client)
|
|
829
834
|
try:
|
|
830
835
|
# main processing loop
|
|
831
836
|
while True:
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyModbusTCP
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A simple Modbus/TCP library for Python
|
|
5
|
+
Home-page: https://github.com/sourceperl/pyModbusTCP
|
|
6
|
+
Author: Loic Lefebvre
|
|
7
|
+
Author-email: loic.celine@free.fr
|
|
8
|
+
License: MIT
|
|
9
|
+
Platform: any
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
|
|
12
|
+
.. |badge_tests| image:: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master
|
|
13
|
+
:target: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml
|
|
14
|
+
|
|
15
|
+
.. |badge_docs| image:: https://readthedocs.org/projects/pymodbustcp/badge/?version=latest
|
|
16
|
+
:target: http://pymodbustcp.readthedocs.io/
|
|
17
|
+
|
|
18
|
+
pyModbusTCP |badge_tests| |badge_docs|
|
|
19
|
+
======================================
|
|
20
|
+
|
|
21
|
+
A simple Modbus/TCP client library for Python.
|
|
22
|
+
pyModbusTCP is pure Python code without any extension or external module dependency.
|
|
23
|
+
|
|
24
|
+
Since version 0.1.0, a server is also available.
|
|
25
|
+
|
|
26
|
+
Tests
|
|
27
|
+
-----
|
|
28
|
+
|
|
29
|
+
The module is currently test on Python 3.8, 3.9, 3.10, 3.11 and 3.12.
|
|
30
|
+
|
|
31
|
+
For Linux, Mac OS and Windows.
|
|
32
|
+
|
|
33
|
+
Documentation
|
|
34
|
+
-------------
|
|
35
|
+
|
|
36
|
+
Documentation of the last release is available online at https://pymodbustcp.readthedocs.io/.
|
|
37
|
+
|
|
38
|
+
Setup
|
|
39
|
+
-----
|
|
40
|
+
|
|
41
|
+
You can install this package from:
|
|
42
|
+
|
|
43
|
+
PyPI, the easy way:
|
|
44
|
+
|
|
45
|
+
.. code-block:: bash
|
|
46
|
+
|
|
47
|
+
# install the last available release (stable)
|
|
48
|
+
sudo pip install pyModbusTCP
|
|
49
|
+
|
|
50
|
+
.. code-block:: bash
|
|
51
|
+
|
|
52
|
+
# install a specific version (here release v0.1.10)
|
|
53
|
+
sudo pip install pyModbusTCP==v0.1.10
|
|
54
|
+
|
|
55
|
+
From GitHub:
|
|
56
|
+
|
|
57
|
+
.. code-block:: bash
|
|
58
|
+
|
|
59
|
+
# install a specific version (here release v0.1.10) directly from github servers
|
|
60
|
+
sudo pip install git+https://github.com/sourceperl/pyModbusTCP.git@v0.1.10
|
|
61
|
+
|
|
62
|
+
Note on the use of versions:
|
|
63
|
+
|
|
64
|
+
Over time, some things can change. So, it's a good practice that you always use a specific version of a package for
|
|
65
|
+
your project, instead of just relying on the default behavior. Without precision, the installation tools will always
|
|
66
|
+
install the latest version available for a package, this may have some drawbacks. For example, in pyModbusTCP, the TCP
|
|
67
|
+
automatic open mode will be active by default from version 0.2.0. It is not the case with previous versions and it just
|
|
68
|
+
doesn't exist before the 0.0.12. This can lead to some strange behaviour of your application if you are not aware of
|
|
69
|
+
the change. Look at `CHANGES <https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES>`_ for details on versions
|
|
70
|
+
available.
|
|
71
|
+
|
|
72
|
+
Usage example
|
|
73
|
+
-------------
|
|
74
|
+
|
|
75
|
+
See examples/ for full scripts.
|
|
76
|
+
|
|
77
|
+
include (for all samples)
|
|
78
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
79
|
+
|
|
80
|
+
.. code-block:: python
|
|
81
|
+
|
|
82
|
+
from pyModbusTCP.client import ModbusClient
|
|
83
|
+
|
|
84
|
+
module init (TCP always open)
|
|
85
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
86
|
+
|
|
87
|
+
.. code-block:: python
|
|
88
|
+
|
|
89
|
+
# TCP auto connect on first modbus request
|
|
90
|
+
c = ModbusClient(host="localhost", port=502, unit_id=1, auto_open=True)
|
|
91
|
+
|
|
92
|
+
module init (TCP open/close for each request)
|
|
93
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
94
|
+
|
|
95
|
+
.. code-block:: python
|
|
96
|
+
|
|
97
|
+
# TCP auto connect on modbus request, close after it
|
|
98
|
+
c = ModbusClient(host="127.0.0.1", auto_open=True, auto_close=True)
|
|
99
|
+
|
|
100
|
+
Read 2x 16 bits registers at modbus address 0 :
|
|
101
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
102
|
+
|
|
103
|
+
.. code-block:: python
|
|
104
|
+
|
|
105
|
+
regs = c.read_holding_registers(0, 2)
|
|
106
|
+
|
|
107
|
+
if regs:
|
|
108
|
+
print(regs)
|
|
109
|
+
else:
|
|
110
|
+
print("read error")
|
|
111
|
+
|
|
112
|
+
Write value 44 and 55 to registers at modbus address 10 :
|
|
113
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
114
|
+
|
|
115
|
+
.. code-block:: python
|
|
116
|
+
|
|
117
|
+
if c.write_multiple_registers(10, [44,55]):
|
|
118
|
+
print("write ok")
|
|
119
|
+
else:
|
|
120
|
+
print("write error")
|
|
@@ -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()
|
pyModbusTCP-0.2.1/PKG-INFO
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 1.0
|
|
2
|
-
Name: pyModbusTCP
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: A simple Modbus/TCP library for Python
|
|
5
|
-
Home-page: https://github.com/sourceperl/pyModbusTCP
|
|
6
|
-
Author: Loic Lefebvre
|
|
7
|
-
Author-email: loic.celine@free.fr
|
|
8
|
-
License: MIT
|
|
9
|
-
Description: .. |badge_tests| image:: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master
|
|
10
|
-
:target: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml
|
|
11
|
-
|
|
12
|
-
.. |badge_docs| image:: https://readthedocs.org/projects/pymodbustcp/badge/?version=latest
|
|
13
|
-
:target: http://pymodbustcp.readthedocs.io/
|
|
14
|
-
|
|
15
|
-
pyModbusTCP |badge_tests| |badge_docs|
|
|
16
|
-
======================================
|
|
17
|
-
|
|
18
|
-
A simple Modbus/TCP client library for Python.
|
|
19
|
-
pyModbusTCP is pure Python code without any extension or external module dependency.
|
|
20
|
-
|
|
21
|
-
Since version 0.1.0, a server is also available for test purpose only (don't use in project).
|
|
22
|
-
|
|
23
|
-
Tests
|
|
24
|
-
-----
|
|
25
|
-
|
|
26
|
-
The module is currently test on Python 3.7, 3.8, 3.9, 3.10 and 3.11.
|
|
27
|
-
|
|
28
|
-
For Linux, Mac OS and Windows.
|
|
29
|
-
|
|
30
|
-
Documentation
|
|
31
|
-
-------------
|
|
32
|
-
|
|
33
|
-
Documentation of the last release is available online at https://pymodbustcp.readthedocs.io/.
|
|
34
|
-
|
|
35
|
-
Setup
|
|
36
|
-
-----
|
|
37
|
-
|
|
38
|
-
You can install this package from:
|
|
39
|
-
|
|
40
|
-
PyPI, the easy way:
|
|
41
|
-
|
|
42
|
-
.. code-block:: bash
|
|
43
|
-
|
|
44
|
-
# install the last available release (stable)
|
|
45
|
-
sudo pip install pyModbusTCP
|
|
46
|
-
|
|
47
|
-
.. code-block:: bash
|
|
48
|
-
|
|
49
|
-
# install a specific version (here release v0.1.10)
|
|
50
|
-
sudo pip install pyModbusTCP==v0.1.10
|
|
51
|
-
|
|
52
|
-
From GitHub:
|
|
53
|
-
|
|
54
|
-
.. code-block:: bash
|
|
55
|
-
|
|
56
|
-
# install a specific version (here release v0.1.10) directly from github servers
|
|
57
|
-
sudo pip install git+https://github.com/sourceperl/pyModbusTCP.git@v0.1.10
|
|
58
|
-
|
|
59
|
-
Note on the use of versions:
|
|
60
|
-
|
|
61
|
-
Over time, some things can change. So, it's a good practice that you always use a specific version of a package for
|
|
62
|
-
your project, instead of just relying on the default behavior. Without precision, the installation tools will always
|
|
63
|
-
install the latest version available for a package, this may have some drawbacks. For example, in pyModbusTCP, the TCP
|
|
64
|
-
automatic open mode will be active by default from version 0.2.0. It is not the case with previous versions and it just
|
|
65
|
-
doesn't exist before the 0.0.12. This can lead to some strange behaviour of your application if you are not aware of
|
|
66
|
-
the change. Look at `CHANGES <https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES>`_ for details on versions
|
|
67
|
-
available.
|
|
68
|
-
|
|
69
|
-
Usage example
|
|
70
|
-
-------------
|
|
71
|
-
|
|
72
|
-
See examples/ for full scripts.
|
|
73
|
-
|
|
74
|
-
include (for all samples)
|
|
75
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
76
|
-
|
|
77
|
-
.. code-block:: python
|
|
78
|
-
|
|
79
|
-
from pyModbusTCP.client import ModbusClient
|
|
80
|
-
|
|
81
|
-
module init (TCP always open)
|
|
82
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
83
|
-
|
|
84
|
-
.. code-block:: python
|
|
85
|
-
|
|
86
|
-
# TCP auto connect on first modbus request
|
|
87
|
-
c = ModbusClient(host="localhost", port=502, unit_id=1, auto_open=True)
|
|
88
|
-
|
|
89
|
-
module init (TCP open/close for each request)
|
|
90
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
91
|
-
|
|
92
|
-
.. code-block:: python
|
|
93
|
-
|
|
94
|
-
# TCP auto connect on modbus request, close after it
|
|
95
|
-
c = ModbusClient(host="127.0.0.1", auto_open=True, auto_close=True)
|
|
96
|
-
|
|
97
|
-
Read 2x 16 bits registers at modbus address 0 :
|
|
98
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
99
|
-
|
|
100
|
-
.. code-block:: python
|
|
101
|
-
|
|
102
|
-
regs = c.read_holding_registers(0, 2)
|
|
103
|
-
|
|
104
|
-
if regs:
|
|
105
|
-
print(regs)
|
|
106
|
-
else:
|
|
107
|
-
print("read error")
|
|
108
|
-
|
|
109
|
-
Write value 44 and 55 to registers at modbus address 10 :
|
|
110
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
111
|
-
|
|
112
|
-
.. code-block:: python
|
|
113
|
-
|
|
114
|
-
if c.write_multiple_registers(10, [44,55]):
|
|
115
|
-
print("write ok")
|
|
116
|
-
else:
|
|
117
|
-
print("write error")
|
|
118
|
-
|
|
119
|
-
Platform: any
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 1.0
|
|
2
|
-
Name: pyModbusTCP
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: A simple Modbus/TCP library for Python
|
|
5
|
-
Home-page: https://github.com/sourceperl/pyModbusTCP
|
|
6
|
-
Author: Loic Lefebvre
|
|
7
|
-
Author-email: loic.celine@free.fr
|
|
8
|
-
License: MIT
|
|
9
|
-
Description: .. |badge_tests| image:: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master
|
|
10
|
-
:target: https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml
|
|
11
|
-
|
|
12
|
-
.. |badge_docs| image:: https://readthedocs.org/projects/pymodbustcp/badge/?version=latest
|
|
13
|
-
:target: http://pymodbustcp.readthedocs.io/
|
|
14
|
-
|
|
15
|
-
pyModbusTCP |badge_tests| |badge_docs|
|
|
16
|
-
======================================
|
|
17
|
-
|
|
18
|
-
A simple Modbus/TCP client library for Python.
|
|
19
|
-
pyModbusTCP is pure Python code without any extension or external module dependency.
|
|
20
|
-
|
|
21
|
-
Since version 0.1.0, a server is also available for test purpose only (don't use in project).
|
|
22
|
-
|
|
23
|
-
Tests
|
|
24
|
-
-----
|
|
25
|
-
|
|
26
|
-
The module is currently test on Python 3.7, 3.8, 3.9, 3.10 and 3.11.
|
|
27
|
-
|
|
28
|
-
For Linux, Mac OS and Windows.
|
|
29
|
-
|
|
30
|
-
Documentation
|
|
31
|
-
-------------
|
|
32
|
-
|
|
33
|
-
Documentation of the last release is available online at https://pymodbustcp.readthedocs.io/.
|
|
34
|
-
|
|
35
|
-
Setup
|
|
36
|
-
-----
|
|
37
|
-
|
|
38
|
-
You can install this package from:
|
|
39
|
-
|
|
40
|
-
PyPI, the easy way:
|
|
41
|
-
|
|
42
|
-
.. code-block:: bash
|
|
43
|
-
|
|
44
|
-
# install the last available release (stable)
|
|
45
|
-
sudo pip install pyModbusTCP
|
|
46
|
-
|
|
47
|
-
.. code-block:: bash
|
|
48
|
-
|
|
49
|
-
# install a specific version (here release v0.1.10)
|
|
50
|
-
sudo pip install pyModbusTCP==v0.1.10
|
|
51
|
-
|
|
52
|
-
From GitHub:
|
|
53
|
-
|
|
54
|
-
.. code-block:: bash
|
|
55
|
-
|
|
56
|
-
# install a specific version (here release v0.1.10) directly from github servers
|
|
57
|
-
sudo pip install git+https://github.com/sourceperl/pyModbusTCP.git@v0.1.10
|
|
58
|
-
|
|
59
|
-
Note on the use of versions:
|
|
60
|
-
|
|
61
|
-
Over time, some things can change. So, it's a good practice that you always use a specific version of a package for
|
|
62
|
-
your project, instead of just relying on the default behavior. Without precision, the installation tools will always
|
|
63
|
-
install the latest version available for a package, this may have some drawbacks. For example, in pyModbusTCP, the TCP
|
|
64
|
-
automatic open mode will be active by default from version 0.2.0. It is not the case with previous versions and it just
|
|
65
|
-
doesn't exist before the 0.0.12. This can lead to some strange behaviour of your application if you are not aware of
|
|
66
|
-
the change. Look at `CHANGES <https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES>`_ for details on versions
|
|
67
|
-
available.
|
|
68
|
-
|
|
69
|
-
Usage example
|
|
70
|
-
-------------
|
|
71
|
-
|
|
72
|
-
See examples/ for full scripts.
|
|
73
|
-
|
|
74
|
-
include (for all samples)
|
|
75
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
76
|
-
|
|
77
|
-
.. code-block:: python
|
|
78
|
-
|
|
79
|
-
from pyModbusTCP.client import ModbusClient
|
|
80
|
-
|
|
81
|
-
module init (TCP always open)
|
|
82
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
83
|
-
|
|
84
|
-
.. code-block:: python
|
|
85
|
-
|
|
86
|
-
# TCP auto connect on first modbus request
|
|
87
|
-
c = ModbusClient(host="localhost", port=502, unit_id=1, auto_open=True)
|
|
88
|
-
|
|
89
|
-
module init (TCP open/close for each request)
|
|
90
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
91
|
-
|
|
92
|
-
.. code-block:: python
|
|
93
|
-
|
|
94
|
-
# TCP auto connect on modbus request, close after it
|
|
95
|
-
c = ModbusClient(host="127.0.0.1", auto_open=True, auto_close=True)
|
|
96
|
-
|
|
97
|
-
Read 2x 16 bits registers at modbus address 0 :
|
|
98
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
99
|
-
|
|
100
|
-
.. code-block:: python
|
|
101
|
-
|
|
102
|
-
regs = c.read_holding_registers(0, 2)
|
|
103
|
-
|
|
104
|
-
if regs:
|
|
105
|
-
print(regs)
|
|
106
|
-
else:
|
|
107
|
-
print("read error")
|
|
108
|
-
|
|
109
|
-
Write value 44 and 55 to registers at modbus address 10 :
|
|
110
|
-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
111
|
-
|
|
112
|
-
.. code-block:: python
|
|
113
|
-
|
|
114
|
-
if c.write_multiple_registers(10, [44,55]):
|
|
115
|
-
print("write ok")
|
|
116
|
-
else:
|
|
117
|
-
print("write error")
|
|
118
|
-
|
|
119
|
-
Platform: any
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|