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.
- hart_sim-0.1.0/CHANGELOG.md +20 -0
- hart_sim-0.1.0/LICENSE +21 -0
- hart_sim-0.1.0/MANIFEST.in +5 -0
- hart_sim-0.1.0/PKG-INFO +345 -0
- hart_sim-0.1.0/README.md +312 -0
- hart_sim-0.1.0/examples/plant.yaml +3 -0
- hart_sim-0.1.0/examples/test_example.py +44 -0
- hart_sim-0.1.0/pyproject.toml +57 -0
- hart_sim-0.1.0/setup.cfg +4 -0
- hart_sim-0.1.0/src/hart_sim/__init__.py +1 -0
- hart_sim-0.1.0/src/hart_sim/__main__.py +5 -0
- hart_sim-0.1.0/src/hart_sim/bench.py +163 -0
- hart_sim-0.1.0/src/hart_sim/cli.py +313 -0
- hart_sim-0.1.0/src/hart_sim/client.py +294 -0
- hart_sim-0.1.0/src/hart_sim/commands/__init__.py +30 -0
- hart_sim-0.1.0/src/hart_sim/commands/common.py +215 -0
- hart_sim-0.1.0/src/hart_sim/commands/universal.py +257 -0
- hart_sim-0.1.0/src/hart_sim/control.py +215 -0
- hart_sim-0.1.0/src/hart_sim/device.py +175 -0
- hart_sim-0.1.0/src/hart_sim/faults.py +263 -0
- hart_sim-0.1.0/src/hart_sim/frame.py +186 -0
- hart_sim-0.1.0/src/hart_sim/hartip.py +57 -0
- hart_sim-0.1.0/src/hart_sim/process.py +145 -0
- hart_sim-0.1.0/src/hart_sim/profile.py +261 -0
- hart_sim-0.1.0/src/hart_sim/profiles/pressure.yaml +16 -0
- hart_sim-0.1.0/src/hart_sim/profiles/temperature.yaml +16 -0
- hart_sim-0.1.0/src/hart_sim/pytest_plugin.py +94 -0
- hart_sim-0.1.0/src/hart_sim/server.py +310 -0
- hart_sim-0.1.0/src/hart_sim/units.py +68 -0
- hart_sim-0.1.0/src/hart_sim.egg-info/PKG-INFO +345 -0
- hart_sim-0.1.0/src/hart_sim.egg-info/SOURCES.txt +49 -0
- hart_sim-0.1.0/src/hart_sim.egg-info/dependency_links.txt +1 -0
- hart_sim-0.1.0/src/hart_sim.egg-info/entry_points.txt +5 -0
- hart_sim-0.1.0/src/hart_sim.egg-info/requires.txt +5 -0
- hart_sim-0.1.0/src/hart_sim.egg-info/top_level.txt +1 -0
- hart_sim-0.1.0/tests/conftest.py +1 -0
- hart_sim-0.1.0/tests/test_bench.py +77 -0
- hart_sim-0.1.0/tests/test_cli.py +242 -0
- hart_sim-0.1.0/tests/test_client.py +107 -0
- hart_sim-0.1.0/tests/test_common.py +345 -0
- hart_sim-0.1.0/tests/test_control.py +158 -0
- hart_sim-0.1.0/tests/test_device.py +161 -0
- hart_sim-0.1.0/tests/test_faults.py +183 -0
- hart_sim-0.1.0/tests/test_frame.py +109 -0
- hart_sim-0.1.0/tests/test_hartip.py +18 -0
- hart_sim-0.1.0/tests/test_plugin.py +118 -0
- hart_sim-0.1.0/tests/test_process.py +150 -0
- hart_sim-0.1.0/tests/test_profile.py +131 -0
- hart_sim-0.1.0/tests/test_server.py +397 -0
- hart_sim-0.1.0/tests/test_tshark.py +175 -0
- 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.
|
hart_sim-0.1.0/PKG-INFO
ADDED
|
@@ -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).
|