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.
Files changed (42) hide show
  1. modbus_sim-0.1.0/CHANGELOG.md +24 -0
  2. modbus_sim-0.1.0/LICENSE +21 -0
  3. modbus_sim-0.1.0/MANIFEST.in +5 -0
  4. modbus_sim-0.1.0/PKG-INFO +337 -0
  5. modbus_sim-0.1.0/README.md +301 -0
  6. modbus_sim-0.1.0/examples/plant.yaml +7 -0
  7. modbus_sim-0.1.0/examples/test_example.py +53 -0
  8. modbus_sim-0.1.0/pyproject.toml +57 -0
  9. modbus_sim-0.1.0/setup.cfg +4 -0
  10. modbus_sim-0.1.0/src/modbus_sim/__init__.py +11 -0
  11. modbus_sim-0.1.0/src/modbus_sim/__main__.py +5 -0
  12. modbus_sim-0.1.0/src/modbus_sim/bench.py +218 -0
  13. modbus_sim-0.1.0/src/modbus_sim/cli.py +373 -0
  14. modbus_sim-0.1.0/src/modbus_sim/client.py +257 -0
  15. modbus_sim-0.1.0/src/modbus_sim/control.py +207 -0
  16. modbus_sim-0.1.0/src/modbus_sim/datatypes.py +123 -0
  17. modbus_sim-0.1.0/src/modbus_sim/device.py +413 -0
  18. modbus_sim-0.1.0/src/modbus_sim/faults.py +197 -0
  19. modbus_sim-0.1.0/src/modbus_sim/frame.py +113 -0
  20. modbus_sim-0.1.0/src/modbus_sim/models.py +431 -0
  21. modbus_sim-0.1.0/src/modbus_sim/profile.py +346 -0
  22. modbus_sim-0.1.0/src/modbus_sim/profiles/energy-meter.yaml +57 -0
  23. modbus_sim-0.1.0/src/modbus_sim/profiles/temperature-controller.yaml +39 -0
  24. modbus_sim-0.1.0/src/modbus_sim/profiles/vfd.yaml +45 -0
  25. modbus_sim-0.1.0/src/modbus_sim/pytest_plugin.py +95 -0
  26. modbus_sim-0.1.0/src/modbus_sim/server.py +368 -0
  27. modbus_sim-0.1.0/src/modbus_sim/signals.py +31 -0
  28. modbus_sim-0.1.0/src/modbus_sim.egg-info/PKG-INFO +337 -0
  29. modbus_sim-0.1.0/src/modbus_sim.egg-info/SOURCES.txt +40 -0
  30. modbus_sim-0.1.0/src/modbus_sim.egg-info/dependency_links.txt +1 -0
  31. modbus_sim-0.1.0/src/modbus_sim.egg-info/entry_points.txt +5 -0
  32. modbus_sim-0.1.0/src/modbus_sim.egg-info/requires.txt +9 -0
  33. modbus_sim-0.1.0/src/modbus_sim.egg-info/top_level.txt +1 -0
  34. modbus_sim-0.1.0/tests/conftest.py +53 -0
  35. modbus_sim-0.1.0/tests/test_cli.py +110 -0
  36. modbus_sim-0.1.0/tests/test_datatypes.py +61 -0
  37. modbus_sim-0.1.0/tests/test_device.py +185 -0
  38. modbus_sim-0.1.0/tests/test_frame.py +44 -0
  39. modbus_sim-0.1.0/tests/test_models.py +186 -0
  40. modbus_sim-0.1.0/tests/test_plugin.py +35 -0
  41. modbus_sim-0.1.0/tests/test_profile.py +67 -0
  42. 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
@@ -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,5 @@
1
+ # What goes into the source distribution besides the package itself
2
+ include CHANGELOG.md LICENSE README.md
3
+ graft tests
4
+ graft examples
5
+ global-exclude __pycache__ *.py[cod]
@@ -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
+ [![CI](https://github.com/steitec/modbus-sim/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/steitec/modbus-sim/actions/workflows/ci.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/modbus-sim)](https://pypi.org/project/modbus-sim/)
46
+ [![Python](https://img.shields.io/pypi/pyversions/modbus-sim)](https://pypi.org/project/modbus-sim/)
47
+ [![License](https://img.shields.io/pypi/l/modbus-sim)](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).