hart-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 (51) hide show
  1. hart_sim-0.1.0/CHANGELOG.md +20 -0
  2. hart_sim-0.1.0/LICENSE +21 -0
  3. hart_sim-0.1.0/MANIFEST.in +5 -0
  4. hart_sim-0.1.0/PKG-INFO +345 -0
  5. hart_sim-0.1.0/README.md +312 -0
  6. hart_sim-0.1.0/examples/plant.yaml +3 -0
  7. hart_sim-0.1.0/examples/test_example.py +44 -0
  8. hart_sim-0.1.0/pyproject.toml +57 -0
  9. hart_sim-0.1.0/setup.cfg +4 -0
  10. hart_sim-0.1.0/src/hart_sim/__init__.py +1 -0
  11. hart_sim-0.1.0/src/hart_sim/__main__.py +5 -0
  12. hart_sim-0.1.0/src/hart_sim/bench.py +163 -0
  13. hart_sim-0.1.0/src/hart_sim/cli.py +313 -0
  14. hart_sim-0.1.0/src/hart_sim/client.py +294 -0
  15. hart_sim-0.1.0/src/hart_sim/commands/__init__.py +30 -0
  16. hart_sim-0.1.0/src/hart_sim/commands/common.py +215 -0
  17. hart_sim-0.1.0/src/hart_sim/commands/universal.py +257 -0
  18. hart_sim-0.1.0/src/hart_sim/control.py +215 -0
  19. hart_sim-0.1.0/src/hart_sim/device.py +175 -0
  20. hart_sim-0.1.0/src/hart_sim/faults.py +263 -0
  21. hart_sim-0.1.0/src/hart_sim/frame.py +186 -0
  22. hart_sim-0.1.0/src/hart_sim/hartip.py +57 -0
  23. hart_sim-0.1.0/src/hart_sim/process.py +145 -0
  24. hart_sim-0.1.0/src/hart_sim/profile.py +261 -0
  25. hart_sim-0.1.0/src/hart_sim/profiles/pressure.yaml +16 -0
  26. hart_sim-0.1.0/src/hart_sim/profiles/temperature.yaml +16 -0
  27. hart_sim-0.1.0/src/hart_sim/pytest_plugin.py +94 -0
  28. hart_sim-0.1.0/src/hart_sim/server.py +310 -0
  29. hart_sim-0.1.0/src/hart_sim/units.py +68 -0
  30. hart_sim-0.1.0/src/hart_sim.egg-info/PKG-INFO +345 -0
  31. hart_sim-0.1.0/src/hart_sim.egg-info/SOURCES.txt +49 -0
  32. hart_sim-0.1.0/src/hart_sim.egg-info/dependency_links.txt +1 -0
  33. hart_sim-0.1.0/src/hart_sim.egg-info/entry_points.txt +5 -0
  34. hart_sim-0.1.0/src/hart_sim.egg-info/requires.txt +5 -0
  35. hart_sim-0.1.0/src/hart_sim.egg-info/top_level.txt +1 -0
  36. hart_sim-0.1.0/tests/conftest.py +1 -0
  37. hart_sim-0.1.0/tests/test_bench.py +77 -0
  38. hart_sim-0.1.0/tests/test_cli.py +242 -0
  39. hart_sim-0.1.0/tests/test_client.py +107 -0
  40. hart_sim-0.1.0/tests/test_common.py +345 -0
  41. hart_sim-0.1.0/tests/test_control.py +158 -0
  42. hart_sim-0.1.0/tests/test_device.py +161 -0
  43. hart_sim-0.1.0/tests/test_faults.py +183 -0
  44. hart_sim-0.1.0/tests/test_frame.py +109 -0
  45. hart_sim-0.1.0/tests/test_hartip.py +18 -0
  46. hart_sim-0.1.0/tests/test_plugin.py +118 -0
  47. hart_sim-0.1.0/tests/test_process.py +150 -0
  48. hart_sim-0.1.0/tests/test_profile.py +131 -0
  49. hart_sim-0.1.0/tests/test_server.py +397 -0
  50. hart_sim-0.1.0/tests/test_tshark.py +175 -0
  51. hart_sim-0.1.0/tests/test_universal.py +237 -0
@@ -0,0 +1,20 @@
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-28
8
+
9
+ - HART-IP version 1 server (TCP and UDP, port 5094) with simulated pressure and
10
+ temperature transmitters (HART revision 7), universal and common practice
11
+ commands, per-master configuration-changed and cold-start bits
12
+ - Process model with 4-20 mA loop current, saturation and NAMUR NE 43 alarm currents
13
+ - YAML profiles and bench files (several devices behind one server)
14
+ - Fault injection (`no_response`, `delay`, `busy`, `comm_error`, `sensor_failure`,
15
+ `saturate`, `cold_start`, `more_status`, `drift`, `write_protect`) through a JSON control
16
+ channel and `hart-sim inject`
17
+ - Command line (`serve`, `poll`, `cmd`, `inject`, `clear`, `set`, `profiles`) and a
18
+ HART-IP client in Python
19
+ - pytest plugin with the `hart_bench` and `hart_bench_factory` fixtures
20
+ - Cross-check of the wire format with the Wireshark HART-IP dissector (`pytest -m tshark`)
hart_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,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,345 @@
1
+ Metadata-Version: 2.4
2
+ Name: hart-sim
3
+ Version: 0.1.0
4
+ Summary: Simulated HART field devices (pressure and temperature transmitters) speaking HART-IP
5
+ Author: Ralf Steinert
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/steitec/hart-sim
8
+ Project-URL: Repository, https://github.com/steitec/hart-sim
9
+ Project-URL: Changelog, https://github.com/steitec/hart-sim/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/steitec/hart-sim/issues
11
+ Keywords: hart,hart-ip,simulator,process automation,field device,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: dev
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: build>=1.2; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # hart-sim
35
+
36
+ Simulated HART field devices, a pressure and a temperature transmitter, that
37
+ speak HART-IP over TCP and UDP on port 5094. Develop and test HART software
38
+ (control-system links, asset management, commissioning scripts, gateways)
39
+ without devices, a HART modem or a commercial simulator, also in CI. The
40
+ devices keep state like real ones, turn a process value into a 4-20 mA loop
41
+ current, and can be made to fail on purpose while they run.
42
+
43
+ ```console
44
+ $ hart-sim serve --bench plant.yaml &
45
+ hart-sim: 2 devices on HART-IP 127.0.0.1:5094 (tcp+udp), control on :5095
46
+ $ hart-sim poll
47
+ addr 1 PT-101 pressure-transmitter id 0x000001
48
+ addr 2 TT-201 temperature-transmitter id 0x000002
49
+ $ hart-sim cmd --tag PT-101 3
50
+ loop 10.70 mA | PV 418.9 kPa | SV 23.5 degC
51
+ $ hart-sim inject PT-101 sensor_failure
52
+ $ hart-sim cmd --tag PT-101 3
53
+ status: device malfunction, more status available, PV out of limits | loop 21.00 mA | PV 416.9 kPa (bad) | SV 23.5 degC
54
+ ```
55
+
56
+ ## Why
57
+
58
+ HART software is usually written against the one real device on the bench, or
59
+ against an expensive simulator, and is rarely tested in CI at all. hart-sim
60
+ gives you devices that:
61
+
62
+ - **are reachable over HART-IP** (TCP and UDP, port 5094), so anything that
63
+ speaks HART-IP can talk to them, with no hardware
64
+ - **keep state** like a real device: tag, range, unit, damping, write
65
+ protect, configuration-change counter, status bits
66
+ - **behave plausibly**: the process value becomes a 4-20 mA loop current with
67
+ saturation and NAMUR NE 43 alarm currents
68
+ - **are strict where real devices are strict**: a bad checksum, a wrong
69
+ address or a malformed frame gets no answer, never a lenient guess
70
+ - **break on purpose**: sensor failure, saturation, busy, communication
71
+ errors, missing answers, drift, injected at runtime through a control
72
+ channel
73
+
74
+ ## Install
75
+
76
+ Requires Python 3.10 or newer; the only dependency is PyYAML.
77
+
78
+ ```bash
79
+ pip install hart-sim
80
+ ```
81
+
82
+ From source, with the test dependencies:
83
+
84
+ ```bash
85
+ git clone https://github.com/steitec/hart-sim.git && cd hart-sim
86
+ pip install -e ".[dev]"
87
+ pytest
88
+ ```
89
+
90
+ ## Quick start
91
+
92
+ ```bash
93
+ hart-sim serve pressure # one device, polling address 0
94
+ hart-sim serve --bench plant.yaml # several devices, see Bench files
95
+ ```
96
+
97
+ `plant.yaml` is the two-device bench shown under [Bench files](#bench-files);
98
+ the repository has it as
99
+ [`examples/plant.yaml`](https://github.com/steitec/hart-sim/blob/main/examples/plant.yaml).
100
+
101
+ The server listens on `127.0.0.1`; HART-IP uses port 5094 (TCP and UDP) and
102
+ the control channel port 5095. Change them with `--host`, `--port` and
103
+ `--control-port` (`0` picks a free port). From a second terminal:
104
+
105
+ ```console
106
+ $ hart-sim poll # which devices answer (command 0 + 13, all 64 addresses)
107
+ $ hart-sim cmd --tag PT-101 3 # command 3 to the device with this tag
108
+ $ hart-sim cmd --address 1 15 # ... or to a polling address
109
+ $ hart-sim cmd --address 1 35 0c4416000000000000 # raw request data as hex
110
+ $ hart-sim inject PT-101 drift per_second=0.5
111
+ $ hart-sim clear PT-101 drift # or: hart-sim clear PT-101 (all faults)
112
+ $ hart-sim set PT-101 pv 123.4 # override the process value; "auto" releases it
113
+ $ hart-sim profiles # bundled profiles
114
+ ```
115
+
116
+ `poll` and `cmd` accept `--host`, `--port` and `--udp`; `inject`, `clear` and
117
+ `set` accept `--host` and `--control-port`. The decoded one-line output covers
118
+ commands 1, 2, 3, 13, 15 and 48; every other command prints the response code,
119
+ the device status and the data as hex.
120
+
121
+ ## Profiles
122
+
123
+ A profile describes one device type in YAML. Two are bundled:
124
+
125
+ | Profile | Device type | PV | SV |
126
+ |---|---|---|---|
127
+ | `pressure` | `0xFA01` | pressure, kPa, sensor limits -100 .. 2500, range 0 .. 1000 | sensor temperature, degC |
128
+ | `temperature` | `0xFA02` | temperature, degC, sensor limits -200 .. 850, range 0 .. 150 | terminal temperature, degC |
129
+
130
+ ```yaml
131
+ kind: pressure-transmitter
132
+ manufacturer_id: 0x00FA # deliberately not a real manufacturer
133
+ device_type: 0xFA01
134
+ device_revision: 1
135
+ tag: PT-101
136
+ long_tag: "Pump 1 discharge pressure"
137
+ descriptor: "SIM PRESSURE"
138
+ min_span: 10
139
+ alarm: high # alarm current: high (21.0 mA) or low (3.6 mA)
140
+ pv:
141
+ unit: kPa
142
+ sensor_limits: [-100, 2500]
143
+ range: [0, 1000] # LRV, URV
144
+ damping: 0.5 # s
145
+ source: {type: sine, offset: 400, amplitude: 50, period: 30}
146
+ sv: {name: sensor temperature, unit: degC, source: {type: constant, value: 23.5}}
147
+ ```
148
+
149
+ Process value sources: `constant` (`value`), `ramp` (`start`, `stop`,
150
+ `period`), `sine` (`offset`, `amplitude`, `period`) and `noise` (`sigma`).
151
+ `source` may also be a list; the values of all its entries are summed:
152
+
153
+ ```yaml
154
+ source:
155
+ - {type: constant, value: 400}
156
+ - {type: sine, offset: 0, amplitude: 20, period: 30}
157
+ - {type: noise, sigma: 0.5}
158
+ ```
159
+
160
+ Units: `inH2O psi bar mbar kPa degC degF K mA %`. The generic profiles use
161
+ manufacturer ID `0x00FA` and expanded device types `0xFA01` / `0xFA02`, which
162
+ deliberately belong to no real manufacturer.
163
+
164
+ ## Bench files
165
+
166
+ A bench puts several devices behind one HART-IP server. Each entry names a
167
+ profile (a bundled name or a YAML file, relative to the bench file), a unique
168
+ polling address (0-63) and a device ID; any other key overrides the profile:
169
+
170
+ ```yaml
171
+ devices:
172
+ - {profile: pressure, polling_address: 1, device_id: 0x000001, tag: PT-101}
173
+ - {profile: temperature, polling_address: 2, device_id: 0x000002, tag: TT-201}
174
+ ```
175
+
176
+ Duplicate tags, polling addresses or device IDs are rejected with the offending
177
+ entry named.
178
+
179
+ ## Supported commands
180
+
181
+ HART revision 7 universal and common practice commands. Write commands are
182
+ refused with response code 7 in write protect mode.
183
+
184
+ | # | Name | |
185
+ |---|---|---|
186
+ | 0 | Read unique identifier | read |
187
+ | 1 | Read primary variable | read |
188
+ | 2 | Read loop current and percent of range | read |
189
+ | 3 | Read dynamic variables and loop current | read |
190
+ | 6 | Write polling address | write |
191
+ | 7 | Read loop configuration | read |
192
+ | 8 | Read dynamic variable classifications | read |
193
+ | 9 | Read device variables with status | read |
194
+ | 11 | Read unique identifier associated with tag | read |
195
+ | 12 | Read message | read |
196
+ | 13 | Read tag, descriptor, date | read |
197
+ | 14 | Read primary variable transducer information | read |
198
+ | 15 | Read device information | read |
199
+ | 16 | Read final assembly number | read |
200
+ | 17 | Write message | write |
201
+ | 18 | Write tag, descriptor, date | write |
202
+ | 19 | Write final assembly number | write |
203
+ | 20 | Read long tag | read |
204
+ | 21 | Read unique identifier associated with long tag | read |
205
+ | 22 | Write long tag | write |
206
+ | 33 | Read device variables | read |
207
+ | 34 | Write primary variable damping value | write |
208
+ | 35 | Write primary variable range values | write |
209
+ | 36 | Set primary variable upper range value | write |
210
+ | 37 | Set primary variable lower range value | write |
211
+ | 38 | Reset configuration changed flag | write |
212
+ | 40 | Enter/exit fixed current mode | write |
213
+ | 41 | Perform device self test | read |
214
+ | 42 | Perform device reset | write |
215
+ | 44 | Write primary variable units | write |
216
+ | 45 | Trim loop current zero | write |
217
+ | 46 | Trim loop current gain | write |
218
+ | 48 | Read additional device status | read |
219
+ | 59 | Write number of response preambles | write |
220
+
221
+ Commands 38 and 48 are not blocked by write protect. Unknown commands get
222
+ response code 64. Requests with too little data get code 5, bad values the
223
+ usual codes (2, 3, 4, 9-12, 14); a request with a bad checksum, for another
224
+ address, or malformed gets no answer.
225
+
226
+ ## Fault injection
227
+
228
+ Faults are injected at runtime with `hart-sim inject` or through the
229
+ [control channel](#control-channel). Parameters are `KEY=VALUE`.
230
+
231
+ | kind | parameters | effect | example |
232
+ |---|---|---|---|
233
+ | `no_response` | `count=1` | the next `count` requests get no answer | `hart-sim inject PT-101 no_response count=3` |
234
+ | `delay` | `seconds`, `count` (default: forever) | answers are sent `seconds` late | `hart-sim inject PT-101 delay seconds=0.5` |
235
+ | `busy` | `count=1` | response code 32 (busy), no state change | `hart-sim inject PT-101 busy count=2` |
236
+ | `comm_error` | `flags=0x08`, `count=1` | communication error byte instead of a response code | `hart-sim inject PT-101 comm_error flags=0x10` |
237
+ | `sensor_failure` | none | device malfunction, PV out of limits and bad, alarm current, details in command 48 | `hart-sim inject PT-101 sensor_failure` |
238
+ | `saturate` | `direction=high` or `low` | loop current pinned to 20.5 / 3.8 mA, "loop current saturated" | `hart-sim inject PT-101 saturate direction=high` |
239
+ | `cold_start` | none | cold start bit set for both masters, as after a restart | `hart-sim inject PT-101 cold_start` |
240
+ | `more_status` | `bytes` (6 integers) | "more status available" until command 48 reads it; the bytes appear in its device specific status | `hart-sim inject PT-101 more_status bytes=1,2,3,4,5,6` |
241
+ | `drift` | `per_second` | the PV drifts away; `clear` removes it | `hart-sim inject PT-101 drift per_second=0.5` |
242
+ | `write_protect` | `on` | write protect switched on or off | `hart-sim inject PT-101 write_protect on=on` |
243
+
244
+ Integers may be written in decimal or as `0x..` hex; booleans as `true`/`false`,
245
+ `on`/`off` or `1`/`0`; `bytes` as a comma-separated list. The same forms work in
246
+ the JSON `params` of the control channel (lists and JSON booleans too).
247
+
248
+ ## Control channel
249
+
250
+ The bench listens on a second TCP port (default 5095) for one JSON object per
251
+ line and answers each with one line, `{"ok": true}` or `{"ok": false, "error": ...}`:
252
+
253
+ ```json
254
+ {"device": "PT-101", "inject": "sensor_failure", "params": {}}
255
+ {"device": "PT-101", "clear": true}
256
+ {"device": "PT-101", "clear": "drift"}
257
+ {"device": "PT-101", "set": {"pv": 123.4}}
258
+ {"list": true}
259
+ ```
260
+
261
+ `set` with `null` returns the PV (or `sv`) to the simulated process; `list`
262
+ returns faults, PV, loop current and polling address per device. `pv` and
263
+ `sv` values must satisfy |value| <= 3.4e38 (what a HART float can carry).
264
+
265
+ The control channel binds `127.0.0.1` whatever `--host` is; use
266
+ `hart-sim serve --control-host ADDRESS` to listen elsewhere. A request line is
267
+ limited to 64 KiB (`{"ok": false, "error": "line too long"}`, then the
268
+ connection is closed).
269
+
270
+ **No authentication:** neither HART-IP nor the control port checks who
271
+ connects. Binding either to `0.0.0.0` (or any non-loopback address) exposes the
272
+ simulated devices to everyone who can reach the machine.
273
+
274
+ ## Python client
275
+
276
+ A small HART-IP master, usable on its own against real gateways too:
277
+
278
+ ```python
279
+ from hart_sim.client import HartIPClient
280
+
281
+ with HartIPClient("127.0.0.1") as client: # transport="udp" for UDP
282
+ device = client.find("PT-101") # command 11 (broadcast by tag)
283
+ print(device.read_variables()) # command 3
284
+ device.write_range(0, 600) # command 35
285
+ print(client.poll(range(8))) # devices at polling addresses 0-7
286
+ ```
287
+
288
+ Lower level: `client.command(address, number, data)` returns the response frame
289
+ (`response_code`, `device_status`, `data`). Communication errors and missing
290
+ answers raise `HartError` / `HartTimeout`.
291
+
292
+ ## pytest plugin
293
+
294
+ Installing the package registers a pytest plugin with a fresh bench for every
295
+ test, on free ports, stopped afterwards:
296
+
297
+ ```python
298
+ def test_range_change(hart_bench):
299
+ with hart_bench.client() as client: # connected HartIPClient
300
+ device = client.handle(0)
301
+ assert device.write_range(0, 600) == 0
302
+
303
+ def test_sensor_failure(hart_bench):
304
+ hart_bench.devices["PT-101"].faults.inject("sensor_failure")
305
+ ...
306
+ ```
307
+
308
+ - `hart_bench`: the bench from `--hart-bench FILE` or the ini key `hart_bench`
309
+ (relative to the rootdir). Without either: one `pressure` device at polling
310
+ address 0 with tag `PT-101`.
311
+ - `hart_bench_factory(path_or_dict_or_profile)`: more benches in one test,
312
+ from a bench file, a bench dict or a bundled profile name.
313
+ - `hart_bench.client(**kwargs)` returns a connected `HartIPClient`
314
+ (`transport`, `timeout`, `primary`); it is closed when the bench stops.
315
+
316
+ See [`examples/test_example.py`](https://github.com/steitec/hart-sim/blob/main/examples/test_example.py).
317
+
318
+ ## Limitations
319
+
320
+ - HART-IP version 1 only, unencrypted. No serial HART (no FSK), no burst
321
+ mode, no device descriptions (DD), no secure connections.
322
+ - The byte layouts of the commands come from public sources, not from the
323
+ paid HART specification. The simulator is not registered with FieldComm
324
+ Group, uses a made-up manufacturer ID and is for development and testing
325
+ only.
326
+ - The long address carries only 14 bits of the expanded device type (`0xFA01`
327
+ is addressed as `0x3A01`).
328
+ - Unit code 1 is inH2O at 68 degF.
329
+ - Commands 36 and 37 apply the same limit checks as 35. A real device's
330
+ command 37 keeps the span.
331
+ - Fixed 4 mA ("multidrop") is only set by command 6 (loop current mode). A
332
+ polling address other than 0 in a bench file or profile does not fix the
333
+ current; this deviates from the wording of the HART specification, which
334
+ ties multidrop to the polling address.
335
+ - Clearing a `drift` fault stops the drift but keeps the offset that has
336
+ already accumulated; it does not return the PV to where it started.
337
+ - The control channel and the bench (`inject`, `clear`, `set`, `list`) address
338
+ a device by its original tag, also after command 18 or 22 has renamed it.
339
+ - HART-IP is IPv4 only.
340
+ - A `delay` fault on UDP delays all UDP clients (single UDP thread).
341
+ - `no_response` is consumed even by requests the device would ignore anyway.
342
+
343
+ ## Licence
344
+
345
+ MIT, see [LICENSE](https://github.com/steitec/hart-sim/blob/main/LICENSE).