pyModbusTCP 0.4.0a1__tar.gz → 0.4.0a2__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.4.0a2/.gitignore +26 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/CHANGES +73 -8
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/PKG-INFO +10 -10
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/README.md +7 -7
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_errors.py +1 -2
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_functions.py +33 -18
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_transport.py +10 -11
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_validators.py +5 -6
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/device_id.py +8 -9
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/modbus_client.py +24 -11
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/constants.py +1 -1
- pymodbustcp-0.4.0a2/pyModbusTCP/server/_common.py +74 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/_functions.py +12 -11
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/_transport.py +85 -57
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/_validators.py +31 -15
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/data_bank.py +16 -19
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/data_handler.py +6 -6
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/device_id.py +8 -9
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/errors.py +0 -4
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/frames.py +5 -5
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/modbus_server.py +57 -29
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/session.py +1 -1
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/__init__.py +1 -1
- pymodbustcp-0.4.0a2/pyModbusTCP/tools/_common.py +125 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/_entry.py +9 -8
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/client.py +30 -22
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/rtu_to_tcp_gw.py +38 -27
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/server.py +10 -6
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/tcp_to_rtu_gw.py +204 -60
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/utils.py +29 -45
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyproject.toml +9 -4
- pymodbustcp-0.4.0a2/tests/test_client.py +468 -0
- pymodbustcp-0.4.0a2/tests/test_client_server.py +382 -0
- pymodbustcp-0.4.0a2/tests/test_server.py +1234 -0
- pymodbustcp-0.4.0a2/tests/test_tools.py +612 -0
- pymodbustcp-0.4.0a2/tests/test_utils.py +291 -0
- pymodbustcp-0.4.0a1/.gitignore +0 -14
- pymodbustcp-0.4.0a1/pyModbusTCP/server/_common.py +0 -13
- pymodbustcp-0.4.0a1/pyModbusTCP/tools/_common.py +0 -69
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/LICENSE +0 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/__init__.py +0 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/__init__.py +0 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_common.py +0 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/py.typed +0 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/__init__.py +0 -0
- {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/__main__.py +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Also read by hatchling (the build backend): these files never go in the sdist or the wheel.
|
|
2
|
+
|
|
3
|
+
# bytecode
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
|
|
7
|
+
# build outputs
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
|
|
12
|
+
# virtual environments
|
|
13
|
+
/venv/
|
|
14
|
+
/.venv/
|
|
15
|
+
|
|
16
|
+
# editors and OS
|
|
17
|
+
/.idea/
|
|
18
|
+
/.vscode/
|
|
19
|
+
.DS_Store
|
|
20
|
+
|
|
21
|
+
# local experiments
|
|
22
|
+
/lab/
|
|
23
|
+
|
|
24
|
+
# Sphinx documentation (built pages and generated help of the tools)
|
|
25
|
+
/docs/_build/
|
|
26
|
+
/docs/_generated/
|
|
@@ -1,11 +1,76 @@
|
|
|
1
1
|
Revision history for pyModbusTCP
|
|
2
2
|
|
|
3
|
-
0.4.
|
|
3
|
+
0.4.0a2 2026-10-17 (alpha: the command line tools, their options and output, may still change before 0.4.0)
|
|
4
|
+
|
|
5
|
+
Changes since 0.4.0a1 (see 0.4.0a1 below for the changes since 0.3.1).
|
|
6
|
+
|
|
7
|
+
Breaking changes:
|
|
8
|
+
|
|
9
|
+
- extras: "pyModbusTCP[tools]" is renamed "pyModbusTCP[cli]" (the mbtcp-* commands of pyModbusTCP-cli).
|
|
10
|
+
- ModbusServer: max_connections_per_host (16) is active by default: a client host (an IPv4 address, or a /64
|
|
11
|
+
network for IPv6) can't open more connections, the next ones are closed at once. Clients
|
|
12
|
+
behind a NAT share the same address: raise it, or set it to None to get the previous behavior.
|
|
13
|
+
- ModbusServer: the constructor checks host (a str) and port (an int from 0 to 65535): TypeError or ValueError,
|
|
14
|
+
a bad value was only seen by start().
|
|
15
|
+
- ModbusClient, DataBank: numpy booleans (numpy.bool_, like the items of a numpy bool array) are no longer
|
|
16
|
+
accepted as coil values (TypeError, like NumPy 2 that refuses them as integers): convert them
|
|
17
|
+
with bool() or tolist(). Numpy integers are still accepted.
|
|
18
|
+
- tools: mbtcp-client exits with status 130 when Ctrl-C interrupts a request (it was 0, a success). Ctrl-C is
|
|
19
|
+
still the normal end of a read repeated with -i (status 0).
|
|
20
|
+
- tools: the delays given on the command line (-t, -e, -i, --idle-timeout) must be greater than 0 and at most
|
|
21
|
+
86400 s (one day), the serial rate (-b) greater than 0, and the slave address of mbtcp-rtu2tcp (-a)
|
|
22
|
+
from 1 to 247: invalid values are refused at startup (status 2).
|
|
23
|
+
|
|
24
|
+
Security:
|
|
25
|
+
|
|
26
|
+
- fix tools: mbtcp-tcp2rtu serves the waiting requests in turn by client host (round robin), instead of one
|
|
27
|
+
FIFO of 5 requests: 3 hosts (with 2 pending requests each) could fill it, every request of the
|
|
28
|
+
other hosts got exception 0x0A (denial of service). A request now waits for at most one request of
|
|
29
|
+
each other host. Up to 16 requests can wait for the serial line (the limit of 2 per host is kept).
|
|
30
|
+
- fix tools: mbtcp-tcp2rtu wrote the requests with an invalid function code (0, or 0x80 to 0xFF: the exception
|
|
31
|
+
flag of a reply) on the serial line (out of --read-only mode): they get exception 0x01 now.
|
|
32
|
+
- fix tools: mbtcp-tcp2rtu no longer sends on the serial line a request that its client session doesn't wait for
|
|
33
|
+
anymore (gateway timeout).
|
|
34
|
+
- fix ModbusServer: request_timeout is a limit for the whole request (MBAP header and PDU) from its first byte, as
|
|
35
|
+
documented: the delay restarted for the PDU, a client could hold a session twice as long.
|
|
36
|
+
- fix ModbusServer: on Windows, the listening port is reserved with SO_EXCLUSIVEADDRUSE instead of SO_REUSEADDR,
|
|
37
|
+
which allowed another local socket to bind the same port and take the connections of the
|
|
38
|
+
server (port hijacking). No change on the other systems.
|
|
39
|
+
- improve ModbusServer: add max_connections_per_host (default 16), a single host can no longer take every
|
|
40
|
+
connection of max_connections (denial of service).
|
|
41
|
+
|
|
42
|
+
Changes:
|
|
43
|
+
|
|
44
|
+
- tools: mbtcp-tcp2rtu limits the pending requests of a client host (at most 2, the next ones get exception
|
|
45
|
+
0x0A). Its refused requests are logged once per 10 s at most (with their count) instead of one line per
|
|
46
|
+
request. New options --max-connections and --max-connections-per-host for mbtcp-tcp2rtu,
|
|
47
|
+
--max-connections-per-host for mbtcp-server.
|
|
48
|
+
- tools: mbtcp-tcp2rtu and mbtcp-rtu2tcp stop cleanly on SIGTERM (systemd, docker stop...), like mbtcp-server.
|
|
49
|
+
- fix tools: -b 0 made mbtcp-tcp2rtu and mbtcp-rtu2tcp crash (ZeroDivisionError), a negative or "nan" timeout
|
|
50
|
+
of mbtcp-tcp2rtu made every request fail, "-a 0" made mbtcp-rtu2tcp a silent slave, "-i inf" made
|
|
51
|
+
mbtcp-client crash (OverflowError).
|
|
52
|
+
- fix utils: valid_host() accepted some non-ASCII chars (like the Kelvin sign "\u212a", taken for a "k" by a
|
|
53
|
+
case-insensitive match), the resolver could then raise a UnicodeError at connect time.
|
|
54
|
+
- fix ModbusClient: a host name that the resolver can't encode (UnicodeError) is a connect error
|
|
55
|
+
(MB_CONNECT_ERR), the request raised the exception.
|
|
56
|
+
- fix ModbusServer: start() no longer leaves the listening socket open when bind() raises something else than
|
|
57
|
+
an OSError (like an OverflowError for a port changed to 70000 after the constructor).
|
|
58
|
+
- fix ModbusServer: a data handler that returns a value that can't be encoded in the response (like a register
|
|
59
|
+
value out of 0-65535, or an exception code over 255) gets a SLAVE_DEVICE_FAILURE (0x04)
|
|
60
|
+
exception, the session was closed.
|
|
61
|
+
- typing: type hints use the Python 3.10 syntax (X | None, list[int]...).
|
|
62
|
+
- build: tools/build_dist.sh -t (install test) ran "python -m pyModbusTCP.cli" (no such module) instead of
|
|
63
|
+
pyModbusTCP.tools, it also checks the release date of the CHANGES entry and the pre-release pins of the docs.
|
|
64
|
+
- build: tools/code_audit.sh runs the tools of the venv (python -m, never from the PATH), from any directory,
|
|
65
|
+
with a summary and an exit status (1 if a check fails), new options -a (with the tests), -s and -o.
|
|
66
|
+
- tests: type hints on the tests, "mypy --strict tests" passes.
|
|
67
|
+
|
|
68
|
+
0.4.0a1 2026-10-15 (alpha: the command line tools, their options and output, may still change before 0.4.0)
|
|
4
69
|
|
|
5
70
|
Breaking changes:
|
|
6
71
|
|
|
7
72
|
- DataBank: remove deprecated class methods (get_bits, set_bits, get_words and set_words).
|
|
8
|
-
- DataBank: set_*() methods raise a TypeError on non-integer items and a ValueError on
|
|
73
|
+
- DataBank: set_*() methods raise a TypeError on non-integer items and a ValueError on register values out of
|
|
9
74
|
the 0-65535 range (values were silently converted before, see below).
|
|
10
75
|
- ModbusClient: non-integer arguments raise a TypeError, custom_request() checks its pdu argument
|
|
11
76
|
(TypeError/ValueError) and auto_open, auto_close and no_delay only accept bool or int values.
|
|
@@ -33,20 +98,20 @@ Revision history for pyModbusTCP
|
|
|
33
98
|
functions, TCP layer, argument checks...) with the same layout for both. No change for the users: same
|
|
34
99
|
imports, same logger names, nested names kept (ModbusServer.PDU, ModbusServer.NetworkError...). Exceptions
|
|
35
100
|
of the server are now shown as pyModbusTCP.server.errors.NetworkError (and so on) in tracebacks.
|
|
36
|
-
- new
|
|
101
|
+
- new package pyModbusTCP.tools: command line tools (Modbus/TCP server and client, TCP to RTU and RTU to TCP
|
|
37
102
|
gateways), run them with "python -m pyModbusTCP.tools {server,client,tcp2rtu,rtu2tcp}". The gateways need
|
|
38
103
|
pyserial, a new optional dependency: "pip install pyModbusTCP[serial]".
|
|
39
104
|
- new package pyModbusTCP-cli (directory of the same name, no code): installs the tools as the commands
|
|
40
105
|
mbtcp-server, mbtcp-client, mbtcp-tcp2rtu and mbtcp-rtu2tcp, with pyserial. Install it with
|
|
41
106
|
"pip install pyModbusTCP[tools]" or "pipx install pyModbusTCP-cli". A plain "pip install pyModbusTCP" puts
|
|
42
107
|
no command in the PATH and still has no dependency.
|
|
43
|
-
- build: migrate from setup.py to pyproject.toml, with the hatchling build backend (no
|
|
44
|
-
no longer in the sdist).
|
|
108
|
+
- build: migrate from setup.py to pyproject.toml, with the hatchling build backend (no MANIFEST.in anymore, tests
|
|
109
|
+
are no longer in the sdist).
|
|
45
110
|
- fix ModbusClient: is_open is now False only for a closed socket (file descriptor 0 is valid).
|
|
46
111
|
- fix ModbusClient: non-integer arguments (float, str...) now raise a TypeError instead of an uncaught struct.error.
|
|
47
112
|
- fix ModbusClient: treat socket.gaierror as MB_CONNECT_ERR (like any other connect error).
|
|
48
|
-
- fix ModbusClient: timeout is now a limit for the whole request (send + receive) and no
|
|
49
|
-
server sending its response byte by byte could hold the client much longer than timeout.
|
|
113
|
+
- fix ModbusClient: timeout is now a limit for the whole request (send + receive) and no longer for each socket I/O.
|
|
114
|
+
A server sending its response byte by byte could hold the client much longer than timeout.
|
|
50
115
|
- fix ModbusClient: validate write_multiple_coils items to reject non-integer values (like 'a', None, [], 1.5),
|
|
51
116
|
matching write_single_coil behavior.
|
|
52
117
|
- fix ModbusClient: write_multiple_coils() and write_multiple_registers() now set last_error (MB_RECV_ERR) when the
|
|
@@ -71,7 +136,7 @@ Revision history for pyModbusTCP
|
|
|
71
136
|
- fix ModbusServer: for functions 0x0F, 0x10 and 0x17 the byte count must exactly match the quantity.
|
|
72
137
|
- fix ModbusServer: Read Device Identification (0x2B/0x0E) compliance with the spec (stream access restarts at the
|
|
73
138
|
first object of the category, an unknown object in individual access gets exception 0x02).
|
|
74
|
-
- fix ModbusServer: support MBAP header length from 2 to
|
|
139
|
+
- fix ModbusServer: support MBAP header length from 2 to 254.
|
|
75
140
|
- fix ModbusServer: limit client info to (address, port) for IPv6 peers.
|
|
76
141
|
- fix DataBank: set_coils(), set_discrete_inputs(), set_holding_registers() and set_input_registers() now check
|
|
77
142
|
their input. Non-integer items (float, str, None...) raise a TypeError (numpy integers and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyModbusTCP
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.0a2
|
|
4
4
|
Summary: A simple Modbus/TCP library for Python
|
|
5
5
|
Project-URL: Documentation, https://pymodbustcp.readthedocs.io/
|
|
6
6
|
Project-URL: Source, https://github.com/sourceperl/pyModbusTCP
|
|
@@ -24,13 +24,13 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
24
24
|
Classifier: Topic :: System :: Networking
|
|
25
25
|
Classifier: Typing :: Typed
|
|
26
26
|
Requires-Python: >=3.10
|
|
27
|
+
Provides-Extra: cli
|
|
28
|
+
Requires-Dist: pymodbustcp-cli; extra == 'cli'
|
|
27
29
|
Provides-Extra: serial
|
|
28
30
|
Requires-Dist: pyserial<4,>=3.5; extra == 'serial'
|
|
29
|
-
Provides-Extra: tools
|
|
30
|
-
Requires-Dist: pymodbustcp-cli; extra == 'tools'
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
33
|
-
# pyModbusTCP [](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml)
|
|
33
|
+
# pyModbusTCP [](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml)
|
|
34
34
|
|
|
35
35
|
A simple Modbus/TCP client and server library for Python.
|
|
36
36
|
|
|
@@ -42,7 +42,7 @@ The module is currently tested on Python 3.10, 3.11, 3.12, 3.13 and 3.14, on Lin
|
|
|
42
42
|
|
|
43
43
|
## Documentation
|
|
44
44
|
|
|
45
|
-
Documentation of the
|
|
45
|
+
Documentation of the latest release is available online at https://pymodbustcp.readthedocs.io/.
|
|
46
46
|
|
|
47
47
|
## Security
|
|
48
48
|
|
|
@@ -63,7 +63,7 @@ source venv/bin/activate
|
|
|
63
63
|
From PyPI, the easy way:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
# install the
|
|
66
|
+
# install the latest available release (stable)
|
|
67
67
|
pip install pyModbusTCP
|
|
68
68
|
```
|
|
69
69
|
|
|
@@ -77,7 +77,7 @@ options and their output may still change. pip never installs a pre-release by d
|
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
79
|
# a specific pre-release
|
|
80
|
-
pip install pyModbusTCP==0.4.
|
|
80
|
+
pip install pyModbusTCP==0.4.0a2
|
|
81
81
|
# or the latest release, pre-releases included
|
|
82
82
|
pip install --pre pyModbusTCP
|
|
83
83
|
```
|
|
@@ -165,8 +165,8 @@ server.start()
|
|
|
165
165
|
|
|
166
166
|
## Command line tools
|
|
167
167
|
|
|
168
|
-
***The tools are new in 0.4.0 (only pre-releases for now): without a version or `--pre`,
|
|
169
|
-
installs the stable 0.3.1, without the tools (pip only shows a warning).***
|
|
168
|
+
***The tools are new in 0.4.0 (only pre-releases for now): without a version or `--pre`,
|
|
169
|
+
`pip install "pyModbusTCP[cli]"` installs the stable 0.3.1, without the tools (pip only shows a warning).***
|
|
170
170
|
|
|
171
171
|
The library comes with command line tools: a Modbus/TCP server, a Modbus/TCP client, and two gateways between
|
|
172
172
|
Modbus/TCP and Modbus/RTU (serial line). They run from the library itself:
|
|
@@ -186,7 +186,7 @@ install the optional package pyModbusTCP-cli. A plain `pip install pyModbusTCP`
|
|
|
186
186
|
|
|
187
187
|
```bash
|
|
188
188
|
# in a virtual environment, with the library
|
|
189
|
-
pip install "pyModbusTCP[
|
|
189
|
+
pip install "pyModbusTCP[cli]==0.4.0a2"
|
|
190
190
|
# or as isolated command line tools
|
|
191
191
|
pipx install pyModbusTCP-cli
|
|
192
192
|
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pyModbusTCP [](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml)
|
|
1
|
+
# pyModbusTCP [](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml)
|
|
2
2
|
|
|
3
3
|
A simple Modbus/TCP client and server library for Python.
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ The module is currently tested on Python 3.10, 3.11, 3.12, 3.13 and 3.14, on Lin
|
|
|
10
10
|
|
|
11
11
|
## Documentation
|
|
12
12
|
|
|
13
|
-
Documentation of the
|
|
13
|
+
Documentation of the latest release is available online at https://pymodbustcp.readthedocs.io/.
|
|
14
14
|
|
|
15
15
|
## Security
|
|
16
16
|
|
|
@@ -31,7 +31,7 @@ source venv/bin/activate
|
|
|
31
31
|
From PyPI, the easy way:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
# install the
|
|
34
|
+
# install the latest available release (stable)
|
|
35
35
|
pip install pyModbusTCP
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -45,7 +45,7 @@ options and their output may still change. pip never installs a pre-release by d
|
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
# a specific pre-release
|
|
48
|
-
pip install pyModbusTCP==0.4.
|
|
48
|
+
pip install pyModbusTCP==0.4.0a2
|
|
49
49
|
# or the latest release, pre-releases included
|
|
50
50
|
pip install --pre pyModbusTCP
|
|
51
51
|
```
|
|
@@ -133,8 +133,8 @@ server.start()
|
|
|
133
133
|
|
|
134
134
|
## Command line tools
|
|
135
135
|
|
|
136
|
-
***The tools are new in 0.4.0 (only pre-releases for now): without a version or `--pre`,
|
|
137
|
-
installs the stable 0.3.1, without the tools (pip only shows a warning).***
|
|
136
|
+
***The tools are new in 0.4.0 (only pre-releases for now): without a version or `--pre`,
|
|
137
|
+
`pip install "pyModbusTCP[cli]"` installs the stable 0.3.1, without the tools (pip only shows a warning).***
|
|
138
138
|
|
|
139
139
|
The library comes with command line tools: a Modbus/TCP server, a Modbus/TCP client, and two gateways between
|
|
140
140
|
Modbus/TCP and Modbus/RTU (serial line). They run from the library itself:
|
|
@@ -154,7 +154,7 @@ install the optional package pyModbusTCP-cli. A plain `pip install pyModbusTCP`
|
|
|
154
154
|
|
|
155
155
|
```bash
|
|
156
156
|
# in a virtual environment, with the library
|
|
157
|
-
pip install "pyModbusTCP[
|
|
157
|
+
pip install "pyModbusTCP[cli]==0.4.0a2"
|
|
158
158
|
# or as isolated command line tools
|
|
159
159
|
pipx install pyModbusTCP-cli
|
|
160
160
|
```
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
class InternalError(Exception):
|
|
5
5
|
""" Base of the internal errors of a request (turned into last_error by the client). """
|
|
6
|
-
pass
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class NetworkError(InternalError):
|
|
@@ -19,5 +18,5 @@ class ModbusExcept(InternalError):
|
|
|
19
18
|
""" Modbus exception returned by the server: code is an EXP_xxx constant. """
|
|
20
19
|
|
|
21
20
|
def __init__(self, code: int):
|
|
22
|
-
super().__init__('modbus exception 0x
|
|
21
|
+
super().__init__(f'modbus exception 0x{code:02X}')
|
|
23
22
|
self.code = code
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
""" pyModbusTCP client: the Modbus functions (build the request PDU, decode the reply PDU). """
|
|
2
2
|
|
|
3
3
|
import struct
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from ..constants import (
|
|
7
8
|
ENCAPSULATED_INTERFACE_TRANSPORT,
|
|
@@ -18,16 +19,30 @@ from ..constants import (
|
|
|
18
19
|
WRITE_SINGLE_COIL,
|
|
19
20
|
WRITE_SINGLE_REGISTER,
|
|
20
21
|
)
|
|
21
|
-
from ..utils import
|
|
22
|
-
|
|
22
|
+
from ..utils import (
|
|
23
|
+
_pack_bits,
|
|
24
|
+
_to_bool,
|
|
25
|
+
_to_bool_list,
|
|
26
|
+
_to_int,
|
|
27
|
+
_to_int_list,
|
|
28
|
+
_unpack_bits,
|
|
29
|
+
byte_length,
|
|
30
|
+
)
|
|
31
|
+
from ._common import (
|
|
32
|
+
MAX_READ_BITS,
|
|
33
|
+
MAX_READ_REGS,
|
|
34
|
+
MAX_WRITE_BITS,
|
|
35
|
+
MAX_WRITE_READ_REGS,
|
|
36
|
+
MAX_WRITE_REGS,
|
|
37
|
+
)
|
|
23
38
|
from ._errors import InternalError, NetworkError
|
|
24
39
|
from ._validators import check_addr_nb, check_regs_values
|
|
25
40
|
from .device_id import DeviceIdentificationResponse
|
|
26
41
|
|
|
27
42
|
|
|
28
|
-
def _decode_regs(data: bytes, reg_nb: int) ->
|
|
43
|
+
def _decode_regs(data: bytes, reg_nb: int) -> list[int]:
|
|
29
44
|
"""Decode the first reg_nb big-endian 16 bits registers of a modbus registers field."""
|
|
30
|
-
return list(struct.unpack('
|
|
45
|
+
return list(struct.unpack(f'>{reg_nb}H', data[:2 * reg_nb]))
|
|
31
46
|
|
|
32
47
|
|
|
33
48
|
class ClientFunctionsMixin:
|
|
@@ -42,7 +57,7 @@ class ClientFunctionsMixin:
|
|
|
42
57
|
def _req_pdu(self, tx_pdu: bytes, rx_min_len: int = 2) -> bytes: ...
|
|
43
58
|
def _req_except_handler(self, _except: Exception) -> None: ...
|
|
44
59
|
|
|
45
|
-
def custom_request(self, pdu:
|
|
60
|
+
def custom_request(self, pdu: bytes | bytearray) -> bytes | None:
|
|
46
61
|
"""Send a custom modbus request.
|
|
47
62
|
|
|
48
63
|
The reply must have the same function code (or the same with the exception flag): otherwise it is an error.
|
|
@@ -59,7 +74,7 @@ class ClientFunctionsMixin:
|
|
|
59
74
|
raise TypeError('pdu must be a bytes-like object')
|
|
60
75
|
pdu = bytes(pdu)
|
|
61
76
|
if not 1 <= len(pdu) <= MAX_PDU_SIZE:
|
|
62
|
-
raise ValueError('pdu length out of range (valid from 1 to
|
|
77
|
+
raise ValueError(f'pdu length out of range (valid from 1 to {MAX_PDU_SIZE} bytes)')
|
|
63
78
|
# make request
|
|
64
79
|
try:
|
|
65
80
|
return self._req_pdu(pdu)
|
|
@@ -68,7 +83,7 @@ class ClientFunctionsMixin:
|
|
|
68
83
|
self._req_except_handler(e)
|
|
69
84
|
return None
|
|
70
85
|
|
|
71
|
-
def _read_bits_req(self, func_code: int, bit_addr: int, bit_nb: int) ->
|
|
86
|
+
def _read_bits_req(self, func_code: int, bit_addr: int, bit_nb: int) -> list[bool] | None:
|
|
72
87
|
"""Common part of READ_COILS and READ_DISCRETE_INPUTS (params are already checked)."""
|
|
73
88
|
try:
|
|
74
89
|
tx_pdu = struct.pack('>BHH', func_code, bit_addr, bit_nb)
|
|
@@ -87,7 +102,7 @@ class ClientFunctionsMixin:
|
|
|
87
102
|
self._req_except_handler(e)
|
|
88
103
|
return None
|
|
89
104
|
|
|
90
|
-
def _read_regs_req(self, func_code: int, reg_addr: int, reg_nb: int) ->
|
|
105
|
+
def _read_regs_req(self, func_code: int, reg_addr: int, reg_nb: int) -> list[int] | None:
|
|
91
106
|
"""Common part of READ_HOLDING_REGISTERS and READ_INPUT_REGISTERS (params are already checked)."""
|
|
92
107
|
try:
|
|
93
108
|
tx_pdu = struct.pack('>BHH', func_code, reg_addr, reg_nb)
|
|
@@ -99,7 +114,7 @@ class ClientFunctionsMixin:
|
|
|
99
114
|
return None
|
|
100
115
|
|
|
101
116
|
@staticmethod
|
|
102
|
-
def _decode_regs_reply(rx_pdu: bytes, reg_nb: int) ->
|
|
117
|
+
def _decode_regs_reply(rx_pdu: bytes, reg_nb: int) -> list[int]:
|
|
103
118
|
"""Decode a [func code, byte count, registers...] reply PDU (functions 0x03, 0x04 and 0x17)."""
|
|
104
119
|
# extract field "byte count"
|
|
105
120
|
byte_count = rx_pdu[1]
|
|
@@ -124,7 +139,7 @@ class ClientFunctionsMixin:
|
|
|
124
139
|
self._req_except_handler(e)
|
|
125
140
|
return False
|
|
126
141
|
|
|
127
|
-
def read_coils(self, bit_addr: int, bit_nb: int = 1) ->
|
|
142
|
+
def read_coils(self, bit_addr: int, bit_nb: int = 1) -> list[bool] | None:
|
|
128
143
|
"""Modbus function READ_COILS (0x01).
|
|
129
144
|
|
|
130
145
|
:param bit_addr: bit address (0 to 65535)
|
|
@@ -141,7 +156,7 @@ class ClientFunctionsMixin:
|
|
|
141
156
|
# make request
|
|
142
157
|
return self._read_bits_req(READ_COILS, bit_addr, bit_nb)
|
|
143
158
|
|
|
144
|
-
def read_discrete_inputs(self, bit_addr: int, bit_nb: int = 1) ->
|
|
159
|
+
def read_discrete_inputs(self, bit_addr: int, bit_nb: int = 1) -> list[bool] | None:
|
|
145
160
|
"""Modbus function READ_DISCRETE_INPUTS (0x02).
|
|
146
161
|
|
|
147
162
|
:param bit_addr: bit address (0 to 65535)
|
|
@@ -158,7 +173,7 @@ class ClientFunctionsMixin:
|
|
|
158
173
|
# make request
|
|
159
174
|
return self._read_bits_req(READ_DISCRETE_INPUTS, bit_addr, bit_nb)
|
|
160
175
|
|
|
161
|
-
def read_holding_registers(self, reg_addr: int, reg_nb: int = 1) ->
|
|
176
|
+
def read_holding_registers(self, reg_addr: int, reg_nb: int = 1) -> list[int] | None:
|
|
162
177
|
"""Modbus function READ_HOLDING_REGISTERS (0x03).
|
|
163
178
|
|
|
164
179
|
:param reg_addr: register address (0 to 65535)
|
|
@@ -175,7 +190,7 @@ class ClientFunctionsMixin:
|
|
|
175
190
|
# make request
|
|
176
191
|
return self._read_regs_req(READ_HOLDING_REGISTERS, reg_addr, reg_nb)
|
|
177
192
|
|
|
178
|
-
def read_input_registers(self, reg_addr: int, reg_nb: int = 1) ->
|
|
193
|
+
def read_input_registers(self, reg_addr: int, reg_nb: int = 1) -> list[int] | None:
|
|
179
194
|
"""Modbus function READ_INPUT_REGISTERS (0x04).
|
|
180
195
|
|
|
181
196
|
:param reg_addr: register address (0 to 65535)
|
|
@@ -193,7 +208,7 @@ class ClientFunctionsMixin:
|
|
|
193
208
|
return self._read_regs_req(READ_INPUT_REGISTERS, reg_addr, reg_nb)
|
|
194
209
|
|
|
195
210
|
def read_device_identification(self, read_code: int = 1,
|
|
196
|
-
object_id: int = 0) ->
|
|
211
|
+
object_id: int = 0) -> DeviceIdentificationResponse | None:
|
|
197
212
|
"""Modbus function Read Device Identification (0x2B/0x0E).
|
|
198
213
|
|
|
199
214
|
:param read_code: read device id code, 1 to 3 for respectively: basic, regular and extended stream access,
|
|
@@ -326,14 +341,14 @@ class ClientFunctionsMixin:
|
|
|
326
341
|
check_addr_nb(regs_addr, len(regs_value), MAX_WRITE_REGS, 'regs_addr', 'number of registers', 'write')
|
|
327
342
|
check_regs_values(regs_value, 'regs_value')
|
|
328
343
|
# build PDU: header + registers part (pack all registers in one call)
|
|
329
|
-
pdu_regs_part = struct.pack('
|
|
344
|
+
pdu_regs_part = struct.pack(f'>{len(regs_value)}H', *regs_value)
|
|
330
345
|
tx_pdu = struct.pack('>BHHB', WRITE_MULTIPLE_REGISTERS, regs_addr, len(regs_value), len(pdu_regs_part))
|
|
331
346
|
tx_pdu += pdu_regs_part
|
|
332
347
|
# make request
|
|
333
348
|
return self._write_req(tx_pdu, regs_addr, len(regs_value))
|
|
334
349
|
|
|
335
350
|
def write_read_multiple_registers(self, write_addr: int, write_values: Iterable[int],
|
|
336
|
-
read_addr: int, read_nb: int = 1) ->
|
|
351
|
+
read_addr: int, read_nb: int = 1) -> list[int] | None:
|
|
337
352
|
"""Modbus function WRITE_READ_MULTIPLE_REGISTERS (0x17).
|
|
338
353
|
|
|
339
354
|
:param write_addr: write registers address (0 to 65535)
|
|
@@ -359,7 +374,7 @@ class ClientFunctionsMixin:
|
|
|
359
374
|
# make request
|
|
360
375
|
try:
|
|
361
376
|
# build PDU: header + registers part (pack all registers in one call)
|
|
362
|
-
pdu_regs_part = struct.pack('
|
|
377
|
+
pdu_regs_part = struct.pack(f'>{len(write_values)}H', *write_values)
|
|
363
378
|
tx_pdu = struct.pack('>BHHHHB', WRITE_READ_MULTIPLE_REGISTERS, read_addr, read_nb,
|
|
364
379
|
write_addr, len(write_values), len(pdu_regs_part))
|
|
365
380
|
tx_pdu += pdu_regs_part
|
|
@@ -6,7 +6,6 @@ import struct
|
|
|
6
6
|
import time
|
|
7
7
|
from binascii import hexlify
|
|
8
8
|
from socket import AF_UNSPEC, IPPROTO_TCP, SOCK_STREAM, TCP_NODELAY
|
|
9
|
-
from typing import Optional
|
|
10
9
|
|
|
11
10
|
from ..constants import (
|
|
12
11
|
MB_CONNECT_ERR,
|
|
@@ -36,7 +35,7 @@ class ClientTransportMixin:
|
|
|
36
35
|
auto_open: bool
|
|
37
36
|
auto_close: bool
|
|
38
37
|
_no_delay: bool
|
|
39
|
-
_sock:
|
|
38
|
+
_sock: socket.socket | None
|
|
40
39
|
_deadline: float
|
|
41
40
|
_transaction_id: int
|
|
42
41
|
|
|
@@ -60,11 +59,12 @@ class ClientTransportMixin:
|
|
|
60
59
|
# AF_UNSPEC -> IPv6 (priority on some system) or 4
|
|
61
60
|
try:
|
|
62
61
|
addr_infos = socket.getaddrinfo(self.host, self.port, AF_UNSPEC, SOCK_STREAM)
|
|
63
|
-
except socket.gaierror as e:
|
|
62
|
+
except (socket.gaierror, UnicodeError) as e:
|
|
64
63
|
# name resolution failure: report it like any other connect error (no exception for the caller)
|
|
65
|
-
|
|
64
|
+
# (UnicodeError: a name that the IDNA codec can't encode, like a label too long once encoded)
|
|
65
|
+
raise NetworkError(MB_CONNECT_ERR, f'host name resolution error: {e}') from None
|
|
66
66
|
# try each address in turn, keep the last failure reason for the error message
|
|
67
|
-
last_err:
|
|
67
|
+
last_err: OSError | None = None
|
|
68
68
|
for af, sock_type, proto, _canon_name, sa in addr_infos:
|
|
69
69
|
try:
|
|
70
70
|
self._sock = socket.socket(af, sock_type, proto)
|
|
@@ -84,7 +84,7 @@ class ClientTransportMixin:
|
|
|
84
84
|
break
|
|
85
85
|
# check connect status
|
|
86
86
|
if not self.is_open:
|
|
87
|
-
reason = 'connection refused' if last_err is None else 'connect error:
|
|
87
|
+
reason = 'connection refused' if last_err is None else f'connect error: {last_err}'
|
|
88
88
|
raise NetworkError(MB_CONNECT_ERR, reason)
|
|
89
89
|
|
|
90
90
|
def close(self) -> None:
|
|
@@ -141,7 +141,7 @@ class ClientTransportMixin:
|
|
|
141
141
|
# restore the full timeout (it's reduced by _recv() to the time remaining in the previous request)
|
|
142
142
|
self._set_sock_timeout(self.timeout)
|
|
143
143
|
self._sock.sendall(frame)
|
|
144
|
-
except
|
|
144
|
+
except TimeoutError:
|
|
145
145
|
self.close()
|
|
146
146
|
raise NetworkError(MB_TIMEOUT_ERR, 'timeout error')
|
|
147
147
|
except OSError:
|
|
@@ -184,7 +184,7 @@ class ClientTransportMixin:
|
|
|
184
184
|
try:
|
|
185
185
|
self._set_sock_timeout(remaining)
|
|
186
186
|
r_buffer = self._sock.recv(size)
|
|
187
|
-
except
|
|
187
|
+
except TimeoutError:
|
|
188
188
|
self.close()
|
|
189
189
|
raise NetworkError(MB_TIMEOUT_ERR, 'timeout error')
|
|
190
190
|
except OSError:
|
|
@@ -246,8 +246,8 @@ class ClientTransportMixin:
|
|
|
246
246
|
# extract function code: it must be the one of the request (with or without the exception flag)
|
|
247
247
|
rx_fc = rx_pdu[0]
|
|
248
248
|
if rx_fc & 0x7f != func_code:
|
|
249
|
-
raise NetworkError(MB_RECV_ERR,
|
|
250
|
-
|
|
249
|
+
raise NetworkError(MB_RECV_ERR,
|
|
250
|
+
f'function code mismatch (0x{rx_fc:02X} for a 0x{func_code:02X} request)')
|
|
251
251
|
# check except status
|
|
252
252
|
if rx_fc >= 0x80:
|
|
253
253
|
exp_code = rx_pdu[1]
|
|
@@ -292,4 +292,3 @@ class ClientTransportMixin:
|
|
|
292
292
|
|
|
293
293
|
def on_tx_rx(self, frame: bytes, is_tx: bool) -> None:
|
|
294
294
|
"""Call for each Tx/Rx (for user purposes)."""
|
|
295
|
-
pass
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
""" pyModbusTCP client: checks of the ModbusClient settings and of the request arguments. """
|
|
2
2
|
|
|
3
3
|
import math
|
|
4
|
-
from typing import List
|
|
5
4
|
|
|
6
5
|
from ..utils import valid_host
|
|
7
6
|
|
|
@@ -12,20 +11,20 @@ def check_addr_nb(addr: int, nb: int, max_nb: int, addr_name: str, nb_name: str,
|
|
|
12
11
|
:raises ValueError: if address, number of items or the end of the area is out of range
|
|
13
12
|
"""
|
|
14
13
|
if not 0 <= addr <= 0xffff:
|
|
15
|
-
raise ValueError('
|
|
14
|
+
raise ValueError(f'{addr_name} out of range (valid from 0 to 65535)')
|
|
16
15
|
if not 1 <= nb <= max_nb:
|
|
17
|
-
raise ValueError('
|
|
16
|
+
raise ValueError(f'{nb_name} out of range (valid from 1 to {max_nb})')
|
|
18
17
|
if addr + nb > 0x10000:
|
|
19
|
-
raise ValueError('
|
|
18
|
+
raise ValueError(f'{action} after end of modbus address space')
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
def check_regs_values(values:
|
|
21
|
+
def check_regs_values(values: list[int], name: str) -> None:
|
|
23
22
|
"""Check that every register value fit in 16 bits (values is a non-empty list of int).
|
|
24
23
|
|
|
25
24
|
:raises ValueError: if a value is out of the 0 to 65535 range
|
|
26
25
|
"""
|
|
27
26
|
if min(values) < 0 or max(values) > 0xffff:
|
|
28
|
-
raise ValueError('
|
|
27
|
+
raise ValueError(f'{name} list contains out of range values')
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
def validate_host(value: object) -> str:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
""" pyModbusTCP client: DeviceIdentificationResponse, result of the read device identification function. """
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Dict, Optional
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
@dataclass
|
|
@@ -20,32 +19,32 @@ class DeviceIdentificationResponse:
|
|
|
20
19
|
conformity_level: int = 0
|
|
21
20
|
more_follows: int = 0
|
|
22
21
|
next_object_id: int = 0
|
|
23
|
-
objects_by_id:
|
|
22
|
+
objects_by_id: dict[int, bytes] = field(default_factory=dict)
|
|
24
23
|
|
|
25
24
|
@property
|
|
26
|
-
def vendor_name(self) ->
|
|
25
|
+
def vendor_name(self) -> bytes | None:
|
|
27
26
|
return self.objects_by_id.get(0x00)
|
|
28
27
|
|
|
29
28
|
@property
|
|
30
|
-
def product_code(self) ->
|
|
29
|
+
def product_code(self) -> bytes | None:
|
|
31
30
|
return self.objects_by_id.get(0x01)
|
|
32
31
|
|
|
33
32
|
@property
|
|
34
|
-
def major_minor_revision(self) ->
|
|
33
|
+
def major_minor_revision(self) -> bytes | None:
|
|
35
34
|
return self.objects_by_id.get(0x02)
|
|
36
35
|
|
|
37
36
|
@property
|
|
38
|
-
def vendor_url(self) ->
|
|
37
|
+
def vendor_url(self) -> bytes | None:
|
|
39
38
|
return self.objects_by_id.get(0x03)
|
|
40
39
|
|
|
41
40
|
@property
|
|
42
|
-
def product_name(self) ->
|
|
41
|
+
def product_name(self) -> bytes | None:
|
|
43
42
|
return self.objects_by_id.get(0x04)
|
|
44
43
|
|
|
45
44
|
@property
|
|
46
|
-
def model_name(self) ->
|
|
45
|
+
def model_name(self) -> bytes | None:
|
|
47
46
|
return self.objects_by_id.get(0x05)
|
|
48
47
|
|
|
49
48
|
@property
|
|
50
|
-
def user_application_name(self) ->
|
|
49
|
+
def user_application_name(self) -> bytes | None:
|
|
51
50
|
return self.objects_by_id.get(0x06)
|