modbus-sim 0.1.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.
- modbus_sim-0.1.0/CHANGELOG.md +24 -0
- modbus_sim-0.1.0/LICENSE +21 -0
- modbus_sim-0.1.0/MANIFEST.in +5 -0
- modbus_sim-0.1.0/PKG-INFO +337 -0
- modbus_sim-0.1.0/README.md +301 -0
- modbus_sim-0.1.0/examples/plant.yaml +7 -0
- modbus_sim-0.1.0/examples/test_example.py +53 -0
- modbus_sim-0.1.0/pyproject.toml +57 -0
- modbus_sim-0.1.0/setup.cfg +4 -0
- modbus_sim-0.1.0/src/modbus_sim/__init__.py +11 -0
- modbus_sim-0.1.0/src/modbus_sim/__main__.py +5 -0
- modbus_sim-0.1.0/src/modbus_sim/bench.py +218 -0
- modbus_sim-0.1.0/src/modbus_sim/cli.py +373 -0
- modbus_sim-0.1.0/src/modbus_sim/client.py +257 -0
- modbus_sim-0.1.0/src/modbus_sim/control.py +207 -0
- modbus_sim-0.1.0/src/modbus_sim/datatypes.py +123 -0
- modbus_sim-0.1.0/src/modbus_sim/device.py +413 -0
- modbus_sim-0.1.0/src/modbus_sim/faults.py +197 -0
- modbus_sim-0.1.0/src/modbus_sim/frame.py +113 -0
- modbus_sim-0.1.0/src/modbus_sim/models.py +431 -0
- modbus_sim-0.1.0/src/modbus_sim/profile.py +346 -0
- modbus_sim-0.1.0/src/modbus_sim/profiles/energy-meter.yaml +57 -0
- modbus_sim-0.1.0/src/modbus_sim/profiles/temperature-controller.yaml +39 -0
- modbus_sim-0.1.0/src/modbus_sim/profiles/vfd.yaml +45 -0
- modbus_sim-0.1.0/src/modbus_sim/pytest_plugin.py +95 -0
- modbus_sim-0.1.0/src/modbus_sim/server.py +368 -0
- modbus_sim-0.1.0/src/modbus_sim/signals.py +31 -0
- modbus_sim-0.1.0/src/modbus_sim.egg-info/PKG-INFO +337 -0
- modbus_sim-0.1.0/src/modbus_sim.egg-info/SOURCES.txt +40 -0
- modbus_sim-0.1.0/src/modbus_sim.egg-info/dependency_links.txt +1 -0
- modbus_sim-0.1.0/src/modbus_sim.egg-info/entry_points.txt +5 -0
- modbus_sim-0.1.0/src/modbus_sim.egg-info/requires.txt +9 -0
- modbus_sim-0.1.0/src/modbus_sim.egg-info/top_level.txt +1 -0
- modbus_sim-0.1.0/tests/conftest.py +53 -0
- modbus_sim-0.1.0/tests/test_cli.py +110 -0
- modbus_sim-0.1.0/tests/test_datatypes.py +61 -0
- modbus_sim-0.1.0/tests/test_device.py +185 -0
- modbus_sim-0.1.0/tests/test_frame.py +44 -0
- modbus_sim-0.1.0/tests/test_models.py +186 -0
- modbus_sim-0.1.0/tests/test_plugin.py +35 -0
- modbus_sim-0.1.0/tests/test_profile.py +67 -0
- modbus_sim-0.1.0/tests/test_server.py +186 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## 0.1.0 - 2026-10-31
|
|
8
|
+
|
|
9
|
+
- Modbus TCP, RTU over TCP, and RTU on a serial port (pyserial) or a virtual
|
|
10
|
+
serial port (pty, Linux/macOS); several devices by unit ID behind one server,
|
|
11
|
+
RTU broadcast
|
|
12
|
+
- Function codes 01-08 (08: sub-function 00), 15, 16, 22, 23 and 43/14 (device
|
|
13
|
+
identification) with the exceptions of the specification
|
|
14
|
+
- YAML profiles: points with type (16/32/64-bit integers, floats, strings, bits),
|
|
15
|
+
word order (ABCD/CDAB/BADC/DCBA), scale, access, limits, signal sources
|
|
16
|
+
- Bundled devices with behaviour: `energy-meter`, `temperature-controller`, `vfd`
|
|
17
|
+
- Bench files for several devices with per-device overrides
|
|
18
|
+
- Fault injection (`no_response`, `delay`, `exception`, `busy`, `drop_connection`,
|
|
19
|
+
`bad_crc`, `wrong_transaction`, `wrong_unit`, `truncate`, `freeze`, `drift`, `trip`,
|
|
20
|
+
`sensor_break`, `phase_loss`) through a JSON control channel and the CLI
|
|
21
|
+
- Command line (`serve`, `profiles`, `map`, `read`, `write`, `ident`, `inject`, `clear`,
|
|
22
|
+
`get`, `set`, `list`) and a small Python client
|
|
23
|
+
- pytest plugin with the `modbus_bench` and `modbus_bench_factory` fixtures
|
|
24
|
+
- Test suite cross-checks the simulator against pymodbus
|
modbus_sim-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ralf Steinert
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modbus-sim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simulated Modbus devices with behaviour and fault injection: energy meter, temperature controller, VFD over Modbus TCP and RTU
|
|
5
|
+
Author: Ralf Steinert
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/steitec/modbus-sim
|
|
8
|
+
Project-URL: Repository, https://github.com/steitec/modbus-sim
|
|
9
|
+
Project-URL: Changelog, https://github.com/steitec/modbus-sim/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/steitec/modbus-sim/issues
|
|
11
|
+
Keywords: modbus,modbus-tcp,modbus-rtu,simulator,slave,scada,industrial automation,test automation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Manufacturing
|
|
15
|
+
Classifier: Framework :: Pytest
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Topic :: System :: Hardware
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: pyyaml>=6
|
|
29
|
+
Provides-Extra: serial
|
|
30
|
+
Requires-Dist: pyserial>=3.5; extra == "serial"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
33
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
34
|
+
Requires-Dist: pymodbus>=3.10; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
<a href="https://steitec.net"><picture>
|
|
38
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/steitec/.github/main/brand/steitec-logo-on-dark.svg">
|
|
39
|
+
<img align="right" height="32" alt="steitec" src="https://raw.githubusercontent.com/steitec/.github/main/brand/steitec-logo-on-light.svg">
|
|
40
|
+
</picture></a>
|
|
41
|
+
|
|
42
|
+
# modbus-sim
|
|
43
|
+
|
|
44
|
+
[](https://github.com/steitec/modbus-sim/actions/workflows/ci.yml)
|
|
45
|
+
[](https://pypi.org/project/modbus-sim/)
|
|
46
|
+
[](https://pypi.org/project/modbus-sim/)
|
|
47
|
+
[](https://github.com/steitec/modbus-sim/blob/main/LICENSE)
|
|
48
|
+
|
|
49
|
+
Simulated Modbus devices that **behave**: an energy meter whose counters count,
|
|
50
|
+
a temperature controller that heats towards its setpoint, a frequency converter
|
|
51
|
+
that ramps up when you set the run bit and trips when you tell it to. They speak
|
|
52
|
+
Modbus TCP, RTU over TCP and RTU on a serial line or a virtual serial port, so
|
|
53
|
+
you can develop and test SCADA links, data loggers, gateways, Node-RED flows or
|
|
54
|
+
your own Modbus code without the hardware, also in CI.
|
|
55
|
+
|
|
56
|
+
```console
|
|
57
|
+
$ modbus-sim serve --bench examples/plant.yaml &
|
|
58
|
+
modbus-sim: 3 devices (units 1, 2, 3) on modbus tcp 127.0.0.1:5020, control on :5021
|
|
59
|
+
unit 1 meter (energy-meter)
|
|
60
|
+
unit 2 oven (temperature-controller)
|
|
61
|
+
unit 3 pump (vfd)
|
|
62
|
+
$ modbus-sim read input 0 -c 3 -t float32 -o CDAB # meter: L1-L3 voltage
|
|
63
|
+
input 0: 230.201 [336c 4366]
|
|
64
|
+
input 2: 229.924 [ec8c 4365]
|
|
65
|
+
input 4: 229.215 [3714 4365]
|
|
66
|
+
$ modbus-sim write holding 1 30 --scale 0.01 -u 3 # pump: 30 Hz reference
|
|
67
|
+
$ modbus-sim write holding 0 1 -u 3 # control word: run
|
|
68
|
+
$ modbus-sim read holding 101 --scale 0.01 -u 3 # two seconds later
|
|
69
|
+
holding 101: 21.09 [083d]
|
|
70
|
+
$ modbus-sim inject pump trip code=4
|
|
71
|
+
$ modbus-sim read holding 105 -u 3 # fault code
|
|
72
|
+
holding 105: 4 [0004]
|
|
73
|
+
$ modbus-sim inject oven no_response
|
|
74
|
+
$ modbus-sim read input 0 -u 2 --timeout 1
|
|
75
|
+
modbus-sim: no (complete) answer
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Why
|
|
79
|
+
|
|
80
|
+
Free Modbus simulators give you a table of registers. Real integrations break
|
|
81
|
+
on other things:
|
|
82
|
+
|
|
83
|
+
- **behaviour**: a drive needs a control word and a ramp before it reports a
|
|
84
|
+
speed, a meter's energy must rise, a controller's process value must follow
|
|
85
|
+
its setpoint. Code that only ever saw static registers has never been tested.
|
|
86
|
+
- **the classic pitfalls**: 32-bit values in **word order** ABCD, CDAB, BADC or
|
|
87
|
+
DCBA; integers **scaled** by 0.1 or 0.01; **gaps** in the register map that
|
|
88
|
+
answer exception 02; request **size limits** that answer exception 03;
|
|
89
|
+
0-based protocol addresses against 1-based "40001" references in the manual.
|
|
90
|
+
- **failures**: devices that do not answer, answer late, answer busy or with
|
|
91
|
+
an exception, send a bad CRC, a wrong transaction or unit ID, a truncated
|
|
92
|
+
frame, or drop the connection. And device faults: a drive trip, a broken
|
|
93
|
+
temperature sensor, a lost phase.
|
|
94
|
+
|
|
95
|
+
modbus-sim gives you all of that from a YAML profile, switchable at runtime.
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
Requires Python 3.10 or newer; the only dependency is PyYAML.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install modbus-sim
|
|
103
|
+
pip install 'modbus-sim[serial]' # also for real serial ports (pyserial)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
From source, with the test dependencies:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
git clone https://github.com/steitec/modbus-sim
|
|
110
|
+
cd modbus-sim
|
|
111
|
+
pip install -e '.[dev,serial]'
|
|
112
|
+
pytest
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Quick start
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
modbus-sim profiles # the bundled profiles
|
|
119
|
+
modbus-sim map vfd # register map of one of them
|
|
120
|
+
modbus-sim serve vfd # one drive, unit 1, Modbus TCP on 127.0.0.1:5020
|
|
121
|
+
modbus-sim serve vfd --host 0.0.0.0 # reachable from the network
|
|
122
|
+
modbus-sim serve vfd --rtu-over-tcp # RTU frames over TCP (as many gateways do)
|
|
123
|
+
modbus-sim serve vfd --pty # RTU on a new virtual serial port (Linux/macOS)
|
|
124
|
+
modbus-sim serve vfd --serial /dev/ttyUSB0 --baud 9600 --parity N
|
|
125
|
+
modbus-sim serve --bench examples/plant.yaml
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Port 502 needs root on Linux, so the default is 5020. With `--pty`, the server
|
|
129
|
+
prints the device to open, for example `/dev/pts/7`; point your Modbus RTU
|
|
130
|
+
master at it. On Windows, use `--serial COM5` with a virtual null-modem pair
|
|
131
|
+
such as com0com.
|
|
132
|
+
|
|
133
|
+
## Profiles
|
|
134
|
+
|
|
135
|
+
| profile | what it is | in particular |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `energy-meter` | three-phase meter: voltages, currents, powers, power factor, frequency, energy import/export | float32 in **CDAB** word order, Wh counters as uint64, reset coil |
|
|
138
|
+
| `temperature-controller` | PI controller heating a first-order process; run/stop, auto/manual, alarm limits | int16 scaled by 0.1; sensor break reads 3276.7 (0x7FFF) |
|
|
139
|
+
| `vfd` | frequency converter: control word, speed reference, accel/decel ramps, status word, current, voltage, rpm, run hours | trip is latched until a fault reset (rising edge) and a new run edge |
|
|
140
|
+
|
|
141
|
+
`modbus-sim map PROFILE` prints the complete register map, with the 0-based
|
|
142
|
+
protocol address and the conventional reference (`400001` = holding
|
|
143
|
+
register 0):
|
|
144
|
+
|
|
145
|
+
```console
|
|
146
|
+
$ modbus-sim map vfd
|
|
147
|
+
table address ref point type scale unit access limits notes
|
|
148
|
+
discrete 0 100001 ready bool ro
|
|
149
|
+
...
|
|
150
|
+
holding 0 400001 control_word uint16 rw 0: run, 1: reverse, 2: fault reset (rising edge)
|
|
151
|
+
holding 1 400002 speed_reference uint16 0.01 Hz rw 0..50
|
|
152
|
+
holding 100 400101 status_word uint16 ro 0: ready, 1: running, 2: at reference, 3: fault, 4: reverse
|
|
153
|
+
holding 101 400102 output_frequency uint16 0.01 Hz ro
|
|
154
|
+
...
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`--markdown` gives the same as a Markdown table for your documentation.
|
|
158
|
+
|
|
159
|
+
### Your own profiles
|
|
160
|
+
|
|
161
|
+
A profile is a YAML file with points. Each point sits in one table at a
|
|
162
|
+
0-based address and gets its value from a model variable, a signal source, or
|
|
163
|
+
a stored value:
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
name: tank
|
|
167
|
+
description: level sensor and two pumps
|
|
168
|
+
identification: {vendor: ACME, product: TANK-1, revision: "2.1"}
|
|
169
|
+
word_order: CDAB # default for 32/64-bit points
|
|
170
|
+
gaps: error # unmapped addresses: exception 02 ("zero" reads 0 instead)
|
|
171
|
+
points:
|
|
172
|
+
- {name: level, table: input, address: 0, type: float32, unit: m,
|
|
173
|
+
source: [{type: sine, offset: 2.0, amplitude: 0.5, period: 120},
|
|
174
|
+
{type: noise, sigma: 0.01}]}
|
|
175
|
+
- {name: temperature, table: input, address: 2, type: int16, scale: 0.1, unit: degC,
|
|
176
|
+
source: {type: ramp, start: 15, stop: 25, period: 600}}
|
|
177
|
+
- {name: pump1, table: coil, address: 0}
|
|
178
|
+
- {name: pump2, table: coil, address: 1, value: true}
|
|
179
|
+
- {name: max_level, table: holding, address: 0, type: uint16, scale: 0.01, unit: m,
|
|
180
|
+
value: 3.5, min: 0.5, max: 4.0}
|
|
181
|
+
- {name: site, table: holding, address: 10, type: string, length: 8, value: "PLANT A"}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- **types**: `bool` (coils, discrete inputs), `uint16`, `int16`, `uint32`,
|
|
185
|
+
`int32`, `float32`, `uint64`, `int64`, `float64`, `string` (with `length` in
|
|
186
|
+
registers)
|
|
187
|
+
- **word_order** per profile or per point: `ABCD` (big endian), `CDAB`, `BADC`, `DCBA`
|
|
188
|
+
- **scale**: register value × scale = engineering value; integers saturate at
|
|
189
|
+
their range like real devices do
|
|
190
|
+
- **access**: `rw` (default for coils and holding registers) or `ro`; writing
|
|
191
|
+
a read-only point is exception 02
|
|
192
|
+
- **min/max**: written values outside are exception 03
|
|
193
|
+
- **sources**: `constant` (value), `ramp` (start, stop, period), `sine`
|
|
194
|
+
(offset, amplitude, period), `noise` (sigma); a list is added up
|
|
195
|
+
- **model**: the three bundled profiles bind points to a model with
|
|
196
|
+
`model: <variable>`; `model_params` tune it (`time_scale` speeds any model up)
|
|
197
|
+
- **bits**: names for the bits of a uint16 register (shown by `map`)
|
|
198
|
+
|
|
199
|
+
A point named `exception_status` provides the byte returned by function 07.
|
|
200
|
+
|
|
201
|
+
## Bench files
|
|
202
|
+
|
|
203
|
+
Several devices behind one server, each with its own unit ID, like a gateway:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
devices:
|
|
207
|
+
- {profile: energy-meter, unit: 1, name: meter}
|
|
208
|
+
- {profile: temperature-controller, unit: 2, name: oven,
|
|
209
|
+
model_params: {setpoint: 180.0, time_constant: 60.0}}
|
|
210
|
+
- {profile: vfd, unit: 3, name: pump, model_params: {accel_time: 5, decel_time: 5}}
|
|
211
|
+
- {profile: profiles/tank.yaml, unit: 10, word_order: ABCD, values: {site: "PLANT B"}}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
An entry can override `word_order`, `gaps`, `model_params`, `identification`
|
|
215
|
+
and `values` (initial values of stored points). Profile paths are relative to
|
|
216
|
+
the bench file.
|
|
217
|
+
|
|
218
|
+
## Protocol
|
|
219
|
+
|
|
220
|
+
| function | name | notes |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| 01, 02 | read coils / discrete inputs | 1-2000 bits |
|
|
223
|
+
| 03, 04 | read holding / input registers | 1-125 registers |
|
|
224
|
+
| 05, 06 | write single coil / register | coil value 0xFF00 or 0x0000 |
|
|
225
|
+
| 07 | read exception status | from the `exception_status` point |
|
|
226
|
+
| 08 | diagnostics | sub-function 00 (return query data) only |
|
|
227
|
+
| 15, 16 | write multiple coils / registers | 1-1968 bits, 1-123 registers |
|
|
228
|
+
| 22 | mask write register | |
|
|
229
|
+
| 23 | read/write multiple registers | write first, then read |
|
|
230
|
+
| 43 / 14 | read device identification | basic and regular objects, stream and individual access |
|
|
231
|
+
|
|
232
|
+
Anything else answers exception 01. Exceptions follow the specification:
|
|
233
|
+
02 for unmapped or read-only addresses and for writes that would change only
|
|
234
|
+
part of a 32/64-bit value, 03 for bad quantities, byte counts and values
|
|
235
|
+
outside `min`/`max`.
|
|
236
|
+
|
|
237
|
+
**Unit IDs.** On Modbus TCP an unknown unit answers exception 0x0B (gateway
|
|
238
|
+
target failed to respond); a single device also answers unit 0 and 255. On
|
|
239
|
+
RTU an unknown unit gets no answer, a frame with a bad CRC is ignored, and
|
|
240
|
+
unit 0 is a broadcast: write requests reach every device and nobody answers.
|
|
241
|
+
|
|
242
|
+
## Fault injection
|
|
243
|
+
|
|
244
|
+
Inject faults into a running bench with `modbus-sim inject DEVICE FAULT
|
|
245
|
+
[name=value ...]` (device by name or unit ID), clear them with `modbus-sim
|
|
246
|
+
clear DEVICE [FAULT]`:
|
|
247
|
+
|
|
248
|
+
| fault | parameters | effect |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `no_response` | count=1 | no answer, the master times out |
|
|
251
|
+
| `delay` | seconds, count=forever | answers come late |
|
|
252
|
+
| `exception` | code, function, count=1 | answers with that exception code, optionally only for one function code |
|
|
253
|
+
| `busy` | count=1 | exception 06 |
|
|
254
|
+
| `drop_connection` | count=1 | the TCP connection is closed instead of an answer |
|
|
255
|
+
| `bad_crc` | count=1 | RTU: the answer has a wrong CRC |
|
|
256
|
+
| `wrong_transaction` | count=1 | TCP: the answer has another transaction ID |
|
|
257
|
+
| `wrong_unit` | count=1 | the answer comes from another unit ID |
|
|
258
|
+
| `truncate` | bytes=1, count=1 | the answer is cut short |
|
|
259
|
+
| `freeze` | | values stop changing |
|
|
260
|
+
| `drift` | point, per_second | a numeric point runs away |
|
|
261
|
+
| `trip` | code=1 | `vfd`: fault trip, coasts to stop, latched until reset |
|
|
262
|
+
| `sensor_break` | | `temperature-controller`: PV reads 3276.7, output off, alarm |
|
|
263
|
+
| `phase_loss` | phase | `energy-meter`: that phase reads 0 V, 0 A |
|
|
264
|
+
|
|
265
|
+
## Control channel
|
|
266
|
+
|
|
267
|
+
`modbus-sim serve` also listens on a control port (default 5021, loopback
|
|
268
|
+
only, no authentication). It takes one JSON object per line:
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{"device": "pump", "inject": "trip", "params": {"code": 7}}
|
|
272
|
+
{"device": "pump", "clear": true}
|
|
273
|
+
{"device": 2, "set": {"pv": 99.5}}
|
|
274
|
+
{"device": "oven", "get": ["pv", "output"]}
|
|
275
|
+
{"list": true}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`set` overrides a point's value (a model or source point keeps it until you
|
|
279
|
+
set `null`); the CLI has the same as `modbus-sim set`, `get` and `list`.
|
|
280
|
+
`--no-control` turns the channel off.
|
|
281
|
+
|
|
282
|
+
## Python client
|
|
283
|
+
|
|
284
|
+
A minimal master is included, mainly for trying things out and for tests:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from modbus_sim.client import ModbusClient
|
|
288
|
+
|
|
289
|
+
with ModbusClient("127.0.0.1", 5020) as c:
|
|
290
|
+
c.write_register(1, 2500, unit=3) # 25.00 Hz
|
|
291
|
+
c.write_register(0, 1, unit=3) # run
|
|
292
|
+
status, frequency = c.read_holding_registers(100, 2, unit=3)
|
|
293
|
+
|
|
294
|
+
with ModbusClient.serial("/dev/pts/7") as c: # RTU on a serial port or pty
|
|
295
|
+
print(c.read_device_identification(unit=1))
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Errors raise `ModbusError` (with `.code`), `TimeoutError` or `ProtocolError`.
|
|
299
|
+
For production code use pymodbus or similar; the test suite cross-checks the
|
|
300
|
+
simulator against pymodbus.
|
|
301
|
+
|
|
302
|
+
## pytest plugin
|
|
303
|
+
|
|
304
|
+
Installing modbus-sim registers two fixtures:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
def test_my_poller(modbus_bench): # one vfd at unit 1, free port
|
|
308
|
+
modbus_bench.inject(1, "busy")
|
|
309
|
+
result = my_poller("127.0.0.1", modbus_bench.port)
|
|
310
|
+
assert result == "device busy"
|
|
311
|
+
|
|
312
|
+
def test_plant(modbus_bench_factory):
|
|
313
|
+
bench = modbus_bench_factory("examples/plant.yaml") # or a dict or a profile
|
|
314
|
+
rtu = modbus_bench_factory("energy-meter", framing="rtu") # RTU over TCP
|
|
315
|
+
with bench.client() as c:
|
|
316
|
+
...
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`--modbus-bench FILE` or the ini key `modbus_bench` sets what `modbus_bench`
|
|
320
|
+
starts. See [examples/test_example.py](examples/test_example.py).
|
|
321
|
+
|
|
322
|
+
## Limitations
|
|
323
|
+
|
|
324
|
+
- For development and testing; not a certified or conformance-tested Modbus
|
|
325
|
+
implementation.
|
|
326
|
+
- The bundled profiles are invented devices with plausible register maps, not
|
|
327
|
+
copies of real products. Models are plausible, not physically accurate.
|
|
328
|
+
- No Modbus ASCII, no Modbus/TCP Security (TLS), no function codes 11, 12, 17,
|
|
329
|
+
20, 21, 24; diagnostics only sub-function 00.
|
|
330
|
+
- RTU framing finds the end of a request from its function code, and the
|
|
331
|
+
3.5-character silence only for unknown function codes; inter-character
|
|
332
|
+
timeouts (t1.5) are not enforced.
|
|
333
|
+
- Over a pty, baud rate and parity are not simulated.
|
|
334
|
+
|
|
335
|
+
## Licence
|
|
336
|
+
|
|
337
|
+
MIT, see [LICENSE](LICENSE).
|