pytrms 0.9.2__py3-none-any.whl → 0.9.5__py3-none-any.whl
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.
- pytrms/__init__.py +42 -38
- pytrms/_base/__init__.py +24 -24
- pytrms/_base/ioniclient.py +32 -32
- pytrms/_base/mqttclient.py +119 -119
- pytrms/_version.py +26 -26
- pytrms/clients/__init__.py +33 -33
- pytrms/clients/db_api.py +200 -183
- pytrms/clients/ioniclient.py +87 -87
- pytrms/clients/modbus.py +532 -528
- pytrms/clients/mqtt.py +820 -797
- pytrms/clients/ssevent.py +82 -82
- pytrms/compose/__init__.py +2 -2
- pytrms/compose/composition.py +302 -302
- pytrms/data/IoniTofPrefs.ini +112 -112
- pytrms/data/ParaIDs.csv +731 -731
- pytrms/helpers.py +126 -120
- pytrms/instrument.py +124 -119
- pytrms/measurement.py +225 -173
- pytrms/peaktable.py +499 -501
- pytrms/plotting/__init__.py +4 -4
- pytrms/plotting/plotting.py +27 -27
- pytrms/readers/__init__.py +4 -4
- pytrms/readers/ionitof_reader.py +472 -472
- {pytrms-0.9.2.dist-info → pytrms-0.9.5.dist-info}/LICENSE +339 -339
- {pytrms-0.9.2.dist-info → pytrms-0.9.5.dist-info}/METADATA +3 -2
- pytrms-0.9.5.dist-info/RECORD +27 -0
- {pytrms-0.9.2.dist-info → pytrms-0.9.5.dist-info}/WHEEL +1 -1
- pytrms-0.9.2.dist-info/RECORD +0 -27
pytrms/clients/modbus.py
CHANGED
|
@@ -1,528 +1,532 @@
|
|
|
1
|
-
"""Module modbus.py
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
import os
|
|
5
|
-
import struct
|
|
6
|
-
import time
|
|
7
|
-
import logging
|
|
8
|
-
from collections import namedtuple
|
|
9
|
-
from functools import lru_cache
|
|
10
|
-
from itertools import tee
|
|
11
|
-
|
|
12
|
-
import pyModbusTCP.client
|
|
13
|
-
|
|
14
|
-
from . import _par_id_file
|
|
15
|
-
from .._base.ioniclient import IoniClientBase
|
|
16
|
-
|
|
17
|
-
log = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
__all__ = ['IoniconModbus']
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _patch_is_open():
|
|
23
|
-
# Note: the .is_open and .timeout attributes were changed
|
|
24
|
-
# from a function to a property!
|
|
25
|
-
#
|
|
26
|
-
# 0.2.0 2022-06-05
|
|
27
|
-
#
|
|
28
|
-
# - ModbusClient: parameters are now properties instead of methods (more intuitive).
|
|
29
|
-
#
|
|
30
|
-
# from the [changelog](https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES):
|
|
31
|
-
major, minor, patch = pyModbusTCP.__version__.split('.')
|
|
32
|
-
if int(minor) < 2:
|
|
33
|
-
return lambda mc: mc.is_open()
|
|
34
|
-
else:
|
|
35
|
-
return lambda mc: mc.is_open
|
|
36
|
-
|
|
37
|
-
_is_open = _patch_is_open()
|
|
38
|
-
|
|
39
|
-
with open(_par_id_file) as f:
|
|
40
|
-
it = iter(f)
|
|
41
|
-
assert next(it).startswith('ID\tName'), ("Modbus parameter file is corrupt: "
|
|
42
|
-
+ f.name
|
|
43
|
-
+ "\n\ntry re-installing the PyTRMS python package to fix it!")
|
|
44
|
-
_id_to_descr = {int(id_): name for id_, name, *_ in (line.strip().split('\t') for line in it)}
|
|
45
|
-
|
|
46
|
-
# look-up-table for c_structs (see docstring of struct-module for more info).
|
|
47
|
-
# Note: almost *all* parameters used by IoniTOF (esp. AME) are 'float', with
|
|
48
|
-
# some exceptions that are 'short' (alive_counter, n_parameters) or explicitly
|
|
49
|
-
# marked to be 'int' (AME_RunNumber, et.c.):
|
|
50
|
-
_fmts = dict([
|
|
51
|
-
('float', '>f'),
|
|
52
|
-
('double', '>d'),
|
|
53
|
-
('short', '>h'),
|
|
54
|
-
('int', '>i'),
|
|
55
|
-
('long', '>q'),
|
|
56
|
-
])
|
|
57
|
-
|
|
58
|
-
_register = namedtuple('register_info', ['n_registers', 'c_format', 'reg_format'])
|
|
59
|
-
_timecycle = namedtuple('timecycle_info', ('rel_cycle', 'abs_cycle', 'abs_time', 'rel_time'))
|
|
60
|
-
_parameter = namedtuple('parameter_info', ('set', 'act', 'par_id', 'state'))
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _get_fmt(c_format):
|
|
64
|
-
if c_format in _fmts:
|
|
65
|
-
c_format = _fmts[c_format]
|
|
66
|
-
|
|
67
|
-
n_registers = max(struct.calcsize(c_format) // 2, 1)
|
|
68
|
-
reg_format = '>' + 'H' * n_registers
|
|
69
|
-
|
|
70
|
-
return _register(n_registers, c_format, reg_format)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _unpack(registers, format='>f'):
|
|
74
|
-
"""Convert a list of register values to a numeric Python value.
|
|
75
|
-
|
|
76
|
-
Depending on 'c_type', the value is packed into two or four
|
|
77
|
-
8-bit registers, for 2-byte (single) and 4-byte (double)
|
|
78
|
-
representation, respectively.
|
|
79
|
-
|
|
80
|
-
>>> _unpack([17448, 0], 'float')
|
|
81
|
-
672.0
|
|
82
|
-
|
|
83
|
-
>>> _unpack([17446, 32768], 'float')
|
|
84
|
-
666.0
|
|
85
|
-
|
|
86
|
-
>>> _unpack([16875, 61191, 54426, 37896], 'double')
|
|
87
|
-
3749199524.83057
|
|
88
|
-
|
|
89
|
-
>>> _unpack([16875, 61191, 54426, 37896], 'long')
|
|
90
|
-
4750153048903029768
|
|
91
|
-
|
|
92
|
-
"""
|
|
93
|
-
n, c_format, reg_format = _get_fmt(format)
|
|
94
|
-
assert n == len(registers), f"c_format '{c_format}' needs [{n}] registers (got [{len(registers)}])"
|
|
95
|
-
|
|
96
|
-
return struct.unpack(c_format, struct.pack(reg_format, *registers))[0]
|
|
97
|
-
|
|
98
|
-
def _pack(value, format='>f'):
|
|
99
|
-
"""Convert floating point 'value' to registers.
|
|
100
|
-
|
|
101
|
-
Depending on 'c_type', the value is packed into two or four
|
|
102
|
-
8-bit registers, for 2-byte (single) and 4-byte (double)
|
|
103
|
-
representation, respectively.
|
|
104
|
-
"""
|
|
105
|
-
_, c_format, reg_format = _get_fmt(format)
|
|
106
|
-
|
|
107
|
-
return struct.unpack(reg_format, struct.pack(c_format, value))
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class IoniconModbus(IoniClientBase):
|
|
111
|
-
|
|
112
|
-
address = dict([
|
|
113
|
-
('server_state', ( 0, '>f', True)), # 0: Not ready, 1: Ready, 2: Startup
|
|
114
|
-
('measure_state', ( 2, '>f', True)), # 0: Not running | 1: running | 2: Just Started | 3: Just Stopped
|
|
115
|
-
('instrument_state', ( 4, '>f', True)), # 0: Not Ok, 1: Ok, 2: Error, 3: Warning
|
|
116
|
-
('alive_counter', ( 6, '>H', True)), # (updated every 500 ms)
|
|
117
|
-
('n_parameters', ( 2000, '>H', True)),
|
|
118
|
-
('tc_raw', ( 4000, '>f', True)),
|
|
119
|
-
('tc_conc', ( 6000, '>f', True)),
|
|
120
|
-
('n_masses', ( 8000, '>f', True)),
|
|
121
|
-
# ('n_corr', ( 7000, '>i', True)), # not implemented?
|
|
122
|
-
('tc_components', (10000, '>f', True)),
|
|
123
|
-
('ame_alarms', (12000, '>f', True)),
|
|
124
|
-
('user_number', (13900, '>i', True)),
|
|
125
|
-
('step_number', (13902, '>i', True)),
|
|
126
|
-
('run_number', (13904, '>i', True)),
|
|
127
|
-
('use_mean', (13906, '>i', True)),
|
|
128
|
-
('action_number', (13912, '>i', True)),
|
|
129
|
-
('version_major', (13918, '>h', True)),
|
|
130
|
-
('version_minor', (13919, '>h', True)),
|
|
131
|
-
('version_patch', (13920, '>h', True)),
|
|
132
|
-
('ame_state', (13914, '>i', True)), # Running 0=Off; 1=On (not implemented!)
|
|
133
|
-
('n_components', (14000, '>f', True)),
|
|
134
|
-
('component_names', (14002, '>f', True)),
|
|
135
|
-
('ame_mean_data', (26000, '>f', True)),
|
|
136
|
-
('n_ame_mean', (26002, '>d', True)),
|
|
137
|
-
])
|
|
138
|
-
|
|
139
|
-
@classmethod
|
|
140
|
-
def use_legacy_input_registers(klaas, use_input_reg = True):
|
|
141
|
-
"""Read from input- instead of holding-registers (legacy method to be compatible with AME1.0)."""
|
|
142
|
-
use_holding = not use_input_reg
|
|
143
|
-
modded = dict()
|
|
144
|
-
for key, vals in klaas.address.items():
|
|
145
|
-
modded[key] = vals[0], vals[1], use_holding
|
|
146
|
-
klaas.address.update(modded)
|
|
147
|
-
|
|
148
|
-
@property
|
|
149
|
-
def _alive_counter(self):
|
|
150
|
-
return self._read_reg(*self.address['alive_counter'])
|
|
151
|
-
|
|
152
|
-
@property
|
|
153
|
-
def is_connected(self):
|
|
154
|
-
if not _is_open(self.mc):
|
|
155
|
-
return False
|
|
156
|
-
|
|
157
|
-
# wait for the IoniTOF alive-counter to change (1 second max)...
|
|
158
|
-
initial_count = self._alive_counter
|
|
159
|
-
timeout_s = 3 # counter should increase every 500 ms, approximately
|
|
160
|
-
started_at = time.monotonic()
|
|
161
|
-
while time.monotonic() < started_at + timeout_s:
|
|
162
|
-
if initial_count != self._alive_counter:
|
|
163
|
-
return True
|
|
164
|
-
|
|
165
|
-
time.sleep(10e-3)
|
|
166
|
-
return False
|
|
167
|
-
|
|
168
|
-
@property
|
|
169
|
-
def is_running(self):
|
|
170
|
-
return 1.0 == self._read_reg(*self.address['measure_state'])
|
|
171
|
-
|
|
172
|
-
@property
|
|
173
|
-
def error_state(self):
|
|
174
|
-
value = self._read_reg(*self.address['instrument_state'])
|
|
175
|
-
return IoniconModbus._instrument_states.get(value, value)
|
|
176
|
-
|
|
177
|
-
_instrument_states = {
|
|
178
|
-
0.0: "Not Okay",
|
|
179
|
-
1.0: "Okay",
|
|
180
|
-
2.0: "Error",
|
|
181
|
-
3.0: "Warning",
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
@property
|
|
185
|
-
def server_state(self):
|
|
186
|
-
value = self._read_reg(*self.address['server_state'])
|
|
187
|
-
return IoniconModbus._server_states.get(value, value)
|
|
188
|
-
|
|
189
|
-
_server_states = {
|
|
190
|
-
0.0: "Not Ready",
|
|
191
|
-
1.0: "Ready",
|
|
192
|
-
2.0: "Startup",
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
def __init__(self, host='localhost', port=502):
|
|
196
|
-
super().__init__(host, port)
|
|
197
|
-
# Note: we patch the behaviour such, that it behaves like pre-0.2
|
|
198
|
-
# (from the time of development of this module), BUT we skip the
|
|
199
|
-
# auto_close-feature for the sake of speed:
|
|
200
|
-
#
|
|
201
|
-
# 0.2.0 2022-06-05
|
|
202
|
-
#
|
|
203
|
-
# - ModbusClient: now TCP auto open mode is active by default (auto_open=True, auto_close=False).
|
|
204
|
-
#
|
|
205
|
-
# from the [changelog](https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES)
|
|
206
|
-
self.mc = pyModbusTCP.client.ModbusClient(host=self.host, port=self.port,
|
|
207
|
-
auto_open = False, auto_close = False
|
|
208
|
-
)
|
|
209
|
-
# try connect immediately:
|
|
210
|
-
try:
|
|
211
|
-
self.connect()
|
|
212
|
-
except TimeoutError as exc:
|
|
213
|
-
log.warn(f"{exc} (retry connecting when the Instrument is set up)")
|
|
214
|
-
self._addresses = {}
|
|
215
|
-
|
|
216
|
-
def connect(self, timeout_s=10):
|
|
217
|
-
log.info(f"[{self}] connecting to Modbus server...")
|
|
218
|
-
# Note: .timeout-attribute changed to a property with 0.2.0 (see comments above)
|
|
219
|
-
if callable(self.mc.timeout):
|
|
220
|
-
self.mc.timeout(timeout_s)
|
|
221
|
-
else:
|
|
222
|
-
self.mc.timeout = timeout_s
|
|
223
|
-
if not self.mc.open():
|
|
224
|
-
raise TimeoutError(f"[{self}] no connection to modbus socket")
|
|
225
|
-
|
|
226
|
-
started_at = time.monotonic()
|
|
227
|
-
while time.monotonic() < started_at + timeout_s:
|
|
228
|
-
if self.is_connected:
|
|
229
|
-
break
|
|
230
|
-
|
|
231
|
-
time.sleep(10e-3)
|
|
232
|
-
else:
|
|
233
|
-
self.disconnect()
|
|
234
|
-
raise TimeoutError(f"[{self}] no connection to IoniTOF");
|
|
235
|
-
|
|
236
|
-
def disconnect(self):
|
|
237
|
-
if _is_open(self.mc):
|
|
238
|
-
self.mc.close()
|
|
239
|
-
|
|
240
|
-
@property
|
|
241
|
-
@lru_cache
|
|
242
|
-
def n_parameters(self):
|
|
243
|
-
return int(self._read_reg(*self.address['n_parameters']))
|
|
244
|
-
|
|
245
|
-
@property
|
|
246
|
-
@lru_cache
|
|
247
|
-
def n_masses(self):
|
|
248
|
-
return int(self._read_reg(*self.address['n_masses']))
|
|
249
|
-
|
|
250
|
-
@property
|
|
251
|
-
@lru_cache
|
|
252
|
-
def n_components(self):
|
|
253
|
-
return int(self._read_reg(*self.address['n_components']))
|
|
254
|
-
|
|
255
|
-
def read_parameter(self, par_name):
|
|
256
|
-
"""Read any previously loaded parameter by name.
|
|
257
|
-
|
|
258
|
-
For example, after calling `.read_components()`, one can call
|
|
259
|
-
`.read_parameter('DPS_Udrift')` to get only this value with no overhead.
|
|
260
|
-
"""
|
|
261
|
-
try:
|
|
262
|
-
return self._read_reg(*self.address[par_name])
|
|
263
|
-
except KeyError as exc:
|
|
264
|
-
raise KeyError("did you call one of .read_instrument_data(), .read_traces(), et.c. first?")
|
|
265
|
-
|
|
266
|
-
def read_instrument_data(self):
|
|
267
|
-
|
|
268
|
-
# Each block consists of 6 registers:
|
|
269
|
-
# Register 1: Parameter ID
|
|
270
|
-
# Register 2-3: Parameter Set Value as float(real)
|
|
271
|
-
# Register 4-5: Parameter Act Value as float(real)
|
|
272
|
-
# Register 6: Parameter state
|
|
273
|
-
start_register = 2001
|
|
274
|
-
blocksize = 6
|
|
275
|
-
superblocksize = 20*blocksize
|
|
276
|
-
|
|
277
|
-
rv = dict()
|
|
278
|
-
# read 20 parameters at once to save transmission..
|
|
279
|
-
for superblock in range(0, blocksize*self.n_parameters, superblocksize):
|
|
280
|
-
offset = start_register + superblock
|
|
281
|
-
# always using input_register
|
|
282
|
-
input_regs = self.mc.read_input_registers(offset, superblocksize)
|
|
283
|
-
# ..and handle one block per parameter:
|
|
284
|
-
for block in range(0, superblocksize, blocksize):
|
|
285
|
-
par_id, set1, set2, act1, act2, state = input_regs[block:block+6]
|
|
286
|
-
par_id = _unpack([par_id], '>H')
|
|
287
|
-
if len(rv) >= self.n_parameters or par_id == 0:
|
|
288
|
-
break
|
|
289
|
-
|
|
290
|
-
try:
|
|
291
|
-
descr = _id_to_descr[par_id]
|
|
292
|
-
except KeyError as exc:
|
|
293
|
-
log.error("par Id %d not in par_ID_list!" % (par_id))
|
|
294
|
-
continue
|
|
295
|
-
|
|
296
|
-
# save the *act-value* in the address-space for faster lookup:
|
|
297
|
-
self.address[descr] = (start_register + superblock + block + 1 + 2, '>f')
|
|
298
|
-
vset = _unpack([set1, set2], '>f')
|
|
299
|
-
vact = _unpack([act1, act2], '>f')
|
|
300
|
-
rv[descr] = _parameter(vset, vact, par_id, state)
|
|
301
|
-
|
|
302
|
-
return rv
|
|
303
|
-
|
|
304
|
-
def write_instrument_data(self, par_id, new_value, timeout_s=10):
|
|
305
|
-
|
|
306
|
-
#
|
|
307
|
-
# Register
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
start_reg
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
def
|
|
414
|
-
start_reg, c_fmt, _is_holding = self.address['
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
'
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
self.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
#
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1
|
+
"""Module modbus.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import struct
|
|
6
|
+
import time
|
|
7
|
+
import logging
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from itertools import tee
|
|
11
|
+
|
|
12
|
+
import pyModbusTCP.client
|
|
13
|
+
|
|
14
|
+
from . import _par_id_file
|
|
15
|
+
from .._base.ioniclient import IoniClientBase
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
__all__ = ['IoniconModbus']
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _patch_is_open():
|
|
23
|
+
# Note: the .is_open and .timeout attributes were changed
|
|
24
|
+
# from a function to a property!
|
|
25
|
+
#
|
|
26
|
+
# 0.2.0 2022-06-05
|
|
27
|
+
#
|
|
28
|
+
# - ModbusClient: parameters are now properties instead of methods (more intuitive).
|
|
29
|
+
#
|
|
30
|
+
# from the [changelog](https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES):
|
|
31
|
+
major, minor, patch = pyModbusTCP.__version__.split('.')
|
|
32
|
+
if int(minor) < 2:
|
|
33
|
+
return lambda mc: mc.is_open()
|
|
34
|
+
else:
|
|
35
|
+
return lambda mc: mc.is_open
|
|
36
|
+
|
|
37
|
+
_is_open = _patch_is_open()
|
|
38
|
+
|
|
39
|
+
with open(_par_id_file) as f:
|
|
40
|
+
it = iter(f)
|
|
41
|
+
assert next(it).startswith('ID\tName'), ("Modbus parameter file is corrupt: "
|
|
42
|
+
+ f.name
|
|
43
|
+
+ "\n\ntry re-installing the PyTRMS python package to fix it!")
|
|
44
|
+
_id_to_descr = {int(id_): name for id_, name, *_ in (line.strip().split('\t') for line in it)}
|
|
45
|
+
|
|
46
|
+
# look-up-table for c_structs (see docstring of struct-module for more info).
|
|
47
|
+
# Note: almost *all* parameters used by IoniTOF (esp. AME) are 'float', with
|
|
48
|
+
# some exceptions that are 'short' (alive_counter, n_parameters) or explicitly
|
|
49
|
+
# marked to be 'int' (AME_RunNumber, et.c.):
|
|
50
|
+
_fmts = dict([
|
|
51
|
+
('float', '>f'),
|
|
52
|
+
('double', '>d'),
|
|
53
|
+
('short', '>h'),
|
|
54
|
+
('int', '>i'),
|
|
55
|
+
('long', '>q'),
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
_register = namedtuple('register_info', ['n_registers', 'c_format', 'reg_format'])
|
|
59
|
+
_timecycle = namedtuple('timecycle_info', ('rel_cycle', 'abs_cycle', 'abs_time', 'rel_time'))
|
|
60
|
+
_parameter = namedtuple('parameter_info', ('set', 'act', 'par_id', 'state'))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_fmt(c_format):
|
|
64
|
+
if c_format in _fmts:
|
|
65
|
+
c_format = _fmts[c_format]
|
|
66
|
+
|
|
67
|
+
n_registers = max(struct.calcsize(c_format) // 2, 1)
|
|
68
|
+
reg_format = '>' + 'H' * n_registers
|
|
69
|
+
|
|
70
|
+
return _register(n_registers, c_format, reg_format)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _unpack(registers, format='>f'):
|
|
74
|
+
"""Convert a list of register values to a numeric Python value.
|
|
75
|
+
|
|
76
|
+
Depending on 'c_type', the value is packed into two or four
|
|
77
|
+
8-bit registers, for 2-byte (single) and 4-byte (double)
|
|
78
|
+
representation, respectively.
|
|
79
|
+
|
|
80
|
+
>>> _unpack([17448, 0], 'float')
|
|
81
|
+
672.0
|
|
82
|
+
|
|
83
|
+
>>> _unpack([17446, 32768], 'float')
|
|
84
|
+
666.0
|
|
85
|
+
|
|
86
|
+
>>> _unpack([16875, 61191, 54426, 37896], 'double')
|
|
87
|
+
3749199524.83057
|
|
88
|
+
|
|
89
|
+
>>> _unpack([16875, 61191, 54426, 37896], 'long')
|
|
90
|
+
4750153048903029768
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
n, c_format, reg_format = _get_fmt(format)
|
|
94
|
+
assert n == len(registers), f"c_format '{c_format}' needs [{n}] registers (got [{len(registers)}])"
|
|
95
|
+
|
|
96
|
+
return struct.unpack(c_format, struct.pack(reg_format, *registers))[0]
|
|
97
|
+
|
|
98
|
+
def _pack(value, format='>f'):
|
|
99
|
+
"""Convert floating point 'value' to registers.
|
|
100
|
+
|
|
101
|
+
Depending on 'c_type', the value is packed into two or four
|
|
102
|
+
8-bit registers, for 2-byte (single) and 4-byte (double)
|
|
103
|
+
representation, respectively.
|
|
104
|
+
"""
|
|
105
|
+
_, c_format, reg_format = _get_fmt(format)
|
|
106
|
+
|
|
107
|
+
return struct.unpack(reg_format, struct.pack(c_format, value))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class IoniconModbus(IoniClientBase):
|
|
111
|
+
|
|
112
|
+
address = dict([
|
|
113
|
+
('server_state', ( 0, '>f', True)), # 0: Not ready, 1: Ready, 2: Startup
|
|
114
|
+
('measure_state', ( 2, '>f', True)), # 0: Not running | 1: running | 2: Just Started | 3: Just Stopped
|
|
115
|
+
('instrument_state', ( 4, '>f', True)), # 0: Not Ok, 1: Ok, 2: Error, 3: Warning
|
|
116
|
+
('alive_counter', ( 6, '>H', True)), # (updated every 500 ms)
|
|
117
|
+
('n_parameters', ( 2000, '>H', True)),
|
|
118
|
+
('tc_raw', ( 4000, '>f', True)),
|
|
119
|
+
('tc_conc', ( 6000, '>f', True)),
|
|
120
|
+
('n_masses', ( 8000, '>f', True)),
|
|
121
|
+
# ('n_corr', ( 7000, '>i', True)), # not implemented?
|
|
122
|
+
('tc_components', (10000, '>f', True)),
|
|
123
|
+
('ame_alarms', (12000, '>f', True)),
|
|
124
|
+
('user_number', (13900, '>i', True)),
|
|
125
|
+
('step_number', (13902, '>i', True)),
|
|
126
|
+
('run_number', (13904, '>i', True)),
|
|
127
|
+
('use_mean', (13906, '>i', True)),
|
|
128
|
+
('action_number', (13912, '>i', True)),
|
|
129
|
+
('version_major', (13918, '>h', True)),
|
|
130
|
+
('version_minor', (13919, '>h', True)),
|
|
131
|
+
('version_patch', (13920, '>h', True)),
|
|
132
|
+
('ame_state', (13914, '>i', True)), # Running 0=Off; 1=On (not implemented!)
|
|
133
|
+
('n_components', (14000, '>f', True)),
|
|
134
|
+
('component_names', (14002, '>f', True)),
|
|
135
|
+
('ame_mean_data', (26000, '>f', True)),
|
|
136
|
+
('n_ame_mean', (26002, '>d', True)),
|
|
137
|
+
])
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def use_legacy_input_registers(klaas, use_input_reg = True):
|
|
141
|
+
"""Read from input- instead of holding-registers (legacy method to be compatible with AME1.0)."""
|
|
142
|
+
use_holding = not use_input_reg
|
|
143
|
+
modded = dict()
|
|
144
|
+
for key, vals in klaas.address.items():
|
|
145
|
+
modded[key] = vals[0], vals[1], use_holding
|
|
146
|
+
klaas.address.update(modded)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def _alive_counter(self):
|
|
150
|
+
return self._read_reg(*self.address['alive_counter'])
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def is_connected(self):
|
|
154
|
+
if not _is_open(self.mc):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
# wait for the IoniTOF alive-counter to change (1 second max)...
|
|
158
|
+
initial_count = self._alive_counter
|
|
159
|
+
timeout_s = 3 # counter should increase every 500 ms, approximately
|
|
160
|
+
started_at = time.monotonic()
|
|
161
|
+
while time.monotonic() < started_at + timeout_s:
|
|
162
|
+
if initial_count != self._alive_counter:
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
time.sleep(10e-3)
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def is_running(self):
|
|
170
|
+
return 1.0 == self._read_reg(*self.address['measure_state'])
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def error_state(self):
|
|
174
|
+
value = self._read_reg(*self.address['instrument_state'])
|
|
175
|
+
return IoniconModbus._instrument_states.get(value, value)
|
|
176
|
+
|
|
177
|
+
_instrument_states = {
|
|
178
|
+
0.0: "Not Okay",
|
|
179
|
+
1.0: "Okay",
|
|
180
|
+
2.0: "Error",
|
|
181
|
+
3.0: "Warning",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def server_state(self):
|
|
186
|
+
value = self._read_reg(*self.address['server_state'])
|
|
187
|
+
return IoniconModbus._server_states.get(value, value)
|
|
188
|
+
|
|
189
|
+
_server_states = {
|
|
190
|
+
0.0: "Not Ready",
|
|
191
|
+
1.0: "Ready",
|
|
192
|
+
2.0: "Startup",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
def __init__(self, host='localhost', port=502):
|
|
196
|
+
super().__init__(host, port)
|
|
197
|
+
# Note: we patch the behaviour such, that it behaves like pre-0.2
|
|
198
|
+
# (from the time of development of this module), BUT we skip the
|
|
199
|
+
# auto_close-feature for the sake of speed:
|
|
200
|
+
#
|
|
201
|
+
# 0.2.0 2022-06-05
|
|
202
|
+
#
|
|
203
|
+
# - ModbusClient: now TCP auto open mode is active by default (auto_open=True, auto_close=False).
|
|
204
|
+
#
|
|
205
|
+
# from the [changelog](https://github.com/sourceperl/pyModbusTCP/blob/master/CHANGES)
|
|
206
|
+
self.mc = pyModbusTCP.client.ModbusClient(host=self.host, port=self.port,
|
|
207
|
+
auto_open = False, auto_close = False
|
|
208
|
+
)
|
|
209
|
+
# try connect immediately:
|
|
210
|
+
try:
|
|
211
|
+
self.connect()
|
|
212
|
+
except TimeoutError as exc:
|
|
213
|
+
log.warn(f"{exc} (retry connecting when the Instrument is set up)")
|
|
214
|
+
self._addresses = {}
|
|
215
|
+
|
|
216
|
+
def connect(self, timeout_s=10):
|
|
217
|
+
log.info(f"[{self}] connecting to Modbus server...")
|
|
218
|
+
# Note: .timeout-attribute changed to a property with 0.2.0 (see comments above)
|
|
219
|
+
if callable(self.mc.timeout):
|
|
220
|
+
self.mc.timeout(timeout_s)
|
|
221
|
+
else:
|
|
222
|
+
self.mc.timeout = timeout_s
|
|
223
|
+
if not self.mc.open():
|
|
224
|
+
raise TimeoutError(f"[{self}] no connection to modbus socket")
|
|
225
|
+
|
|
226
|
+
started_at = time.monotonic()
|
|
227
|
+
while time.monotonic() < started_at + timeout_s:
|
|
228
|
+
if self.is_connected:
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
time.sleep(10e-3)
|
|
232
|
+
else:
|
|
233
|
+
self.disconnect()
|
|
234
|
+
raise TimeoutError(f"[{self}] no connection to IoniTOF");
|
|
235
|
+
|
|
236
|
+
def disconnect(self):
|
|
237
|
+
if _is_open(self.mc):
|
|
238
|
+
self.mc.close()
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
@lru_cache
|
|
242
|
+
def n_parameters(self):
|
|
243
|
+
return int(self._read_reg(*self.address['n_parameters']))
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
@lru_cache
|
|
247
|
+
def n_masses(self):
|
|
248
|
+
return int(self._read_reg(*self.address['n_masses']))
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
@lru_cache
|
|
252
|
+
def n_components(self):
|
|
253
|
+
return int(self._read_reg(*self.address['n_components']))
|
|
254
|
+
|
|
255
|
+
def read_parameter(self, par_name):
|
|
256
|
+
"""Read any previously loaded parameter by name.
|
|
257
|
+
|
|
258
|
+
For example, after calling `.read_components()`, one can call
|
|
259
|
+
`.read_parameter('DPS_Udrift')` to get only this value with no overhead.
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
return self._read_reg(*self.address[par_name])
|
|
263
|
+
except KeyError as exc:
|
|
264
|
+
raise KeyError("did you call one of .read_instrument_data(), .read_traces(), et.c. first?")
|
|
265
|
+
|
|
266
|
+
def read_instrument_data(self):
|
|
267
|
+
|
|
268
|
+
# Each block consists of 6 registers:
|
|
269
|
+
# Register 1: Parameter ID
|
|
270
|
+
# Register 2-3: Parameter Set Value as float(real)
|
|
271
|
+
# Register 4-5: Parameter Act Value as float(real)
|
|
272
|
+
# Register 6: Parameter state
|
|
273
|
+
start_register = 2001
|
|
274
|
+
blocksize = 6
|
|
275
|
+
superblocksize = 20*blocksize
|
|
276
|
+
|
|
277
|
+
rv = dict()
|
|
278
|
+
# read 20 parameters at once to save transmission..
|
|
279
|
+
for superblock in range(0, blocksize*self.n_parameters, superblocksize):
|
|
280
|
+
offset = start_register + superblock
|
|
281
|
+
# always using input_register
|
|
282
|
+
input_regs = self.mc.read_input_registers(offset, superblocksize)
|
|
283
|
+
# ..and handle one block per parameter:
|
|
284
|
+
for block in range(0, superblocksize, blocksize):
|
|
285
|
+
par_id, set1, set2, act1, act2, state = input_regs[block:block+6]
|
|
286
|
+
par_id = _unpack([par_id], '>H')
|
|
287
|
+
if len(rv) >= self.n_parameters or par_id == 0:
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
descr = _id_to_descr[par_id]
|
|
292
|
+
except KeyError as exc:
|
|
293
|
+
log.error("par Id %d not in par_ID_list!" % (par_id))
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# save the *act-value* in the address-space for faster lookup:
|
|
297
|
+
self.address[descr] = (start_register + superblock + block + 1 + 2, '>f')
|
|
298
|
+
vset = _unpack([set1, set2], '>f')
|
|
299
|
+
vact = _unpack([act1, act2], '>f')
|
|
300
|
+
rv[descr] = _parameter(vset, vact, par_id, state)
|
|
301
|
+
|
|
302
|
+
return rv
|
|
303
|
+
|
|
304
|
+
def write_instrument_data(self, par_id, new_value, timeout_s=10):
|
|
305
|
+
|
|
306
|
+
# This command-structure is written as an array:
|
|
307
|
+
# Register 0: number of command-blocks to write
|
|
308
|
+
# Each command-block consists of 3 registers:
|
|
309
|
+
# Register 1: Parameter ID
|
|
310
|
+
# Register 2-3: Parameter Set Value as float(real)
|
|
311
|
+
start_register = 41000
|
|
312
|
+
blocksize = 3
|
|
313
|
+
|
|
314
|
+
if isinstance(par_id, str):
|
|
315
|
+
try:
|
|
316
|
+
par_id = next(k for k, v in _id_to_descr.items() if v == par_id)
|
|
317
|
+
except StopIteration as exc:
|
|
318
|
+
raise KeyError(str(par_id))
|
|
319
|
+
par_id = int(par_id)
|
|
320
|
+
if par_id not in _id_to_descr:
|
|
321
|
+
raise IndexError(str(par_id))
|
|
322
|
+
|
|
323
|
+
# Note: we use only the first command-block for writing:
|
|
324
|
+
n_blocks = 1
|
|
325
|
+
reg_values = list(_pack(n_blocks, '>h'))
|
|
326
|
+
# ...although we could add more command-blocks here:
|
|
327
|
+
reg_values += list(_pack(par_id, '>h')) + list(_pack(new_value, '>f'))
|
|
328
|
+
|
|
329
|
+
# wait for instrument to receive...
|
|
330
|
+
retry_time = 0
|
|
331
|
+
while retry_time < timeout_s:
|
|
332
|
+
# a value of 0 indicates ready-to-write:
|
|
333
|
+
if self.mc.read_holding_registers(start_register) == [0]:
|
|
334
|
+
break
|
|
335
|
+
retry_time += 0.5
|
|
336
|
+
time.sleep(0.5)
|
|
337
|
+
else:
|
|
338
|
+
raise TimeoutError(f'instrument not ready for writing after ({timeout_s}) seconds')
|
|
339
|
+
|
|
340
|
+
self.mc.write_multiple_registers(start_register, reg_values)
|
|
341
|
+
|
|
342
|
+
@lru_cache
|
|
343
|
+
def read_masses(self, update_address_at=None, with_format='>f'):
|
|
344
|
+
start_reg, c_fmt, _ = self.address['n_masses']
|
|
345
|
+
start_reg += 2 # number of components as float..
|
|
346
|
+
masses = self._read_reg_multi(start_reg, c_fmt, self.n_masses) # input_register
|
|
347
|
+
if update_address_at:
|
|
348
|
+
n_bytes, c_fmt, _ = _get_fmt(with_format)
|
|
349
|
+
self.address.update({
|
|
350
|
+
"{0:.4}".format(mass): (update_address_at + i * n_bytes, c_fmt)
|
|
351
|
+
for i, mass in enumerate(masses)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
return masses
|
|
355
|
+
|
|
356
|
+
def read_traces(self, kind='conc'):
|
|
357
|
+
"""Returns the current traces, where `kind` is one of 'conc', 'raw', 'components'.
|
|
358
|
+
"""
|
|
359
|
+
start_reg, c_fmt, _is_holding = self.address['tc_' + kind]
|
|
360
|
+
start_reg += 14 # skipping timecycles..
|
|
361
|
+
|
|
362
|
+
# update the address-space with the current kind
|
|
363
|
+
# for later calls to `.read_parameter()`:
|
|
364
|
+
masses = self.read_masses(update_address_at=start_reg)
|
|
365
|
+
values = self._read_reg_multi(start_reg, c_fmt, self.n_masses, _is_holding)
|
|
366
|
+
|
|
367
|
+
return dict(zip(
|
|
368
|
+
("{0:.4}".format(mass) for mass in masses), values
|
|
369
|
+
))
|
|
370
|
+
|
|
371
|
+
def read_timecycle(self, kind='conc'):
|
|
372
|
+
"""Returns the current timecycle, where `kind` is one of conc|raw|components.
|
|
373
|
+
|
|
374
|
+
Absolute time as double (8 bytes), seconds since 01.01.1904, 01:00 am.
|
|
375
|
+
Relative time as double (8 bytes) in seconds since measurement start.
|
|
376
|
+
"""
|
|
377
|
+
start_reg, _, _is_holding = self.address['tc_' + kind]
|
|
378
|
+
|
|
379
|
+
return _timecycle(
|
|
380
|
+
int( self._read_reg(start_reg + 2, '>f', _is_holding)),
|
|
381
|
+
int( self._read_reg(start_reg + 4, '>f', _is_holding)),
|
|
382
|
+
float(self._read_reg(start_reg + 6, '>d', _is_holding)),
|
|
383
|
+
float(self._read_reg(start_reg + 10, '>d', _is_holding)),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
@lru_cache
|
|
387
|
+
def read_component_names(self):
|
|
388
|
+
# 14002 ff: Component Names (maximum length per name:
|
|
389
|
+
# 32 chars (=16 registers), e.g. 14018 to 14033: "Isoprene")
|
|
390
|
+
name_start_reg, _, _is_holding = self.address['component_names']
|
|
391
|
+
data_start_reg, c_fmt, _ = self.address['tc_components']
|
|
392
|
+
data_start_reg += 14 # skip timecycle info..
|
|
393
|
+
|
|
394
|
+
_read = self.mc.read_holding_registers if _is_holding else self.mc.read_input_registers
|
|
395
|
+
rv = []
|
|
396
|
+
for index in range(self.n_components):
|
|
397
|
+
n_bytes, c_format, reg_format = _get_fmt('>16H')
|
|
398
|
+
register = _read(name_start_reg + index * 16, n_bytes)
|
|
399
|
+
chars = struct.pack(reg_format, *register)
|
|
400
|
+
decoded = chars.decode('latin-1').strip('\x00')
|
|
401
|
+
self.address[decoded] = (data_start_reg + index * 2, c_fmt, _is_holding)
|
|
402
|
+
rv.append(decoded)
|
|
403
|
+
|
|
404
|
+
return rv
|
|
405
|
+
|
|
406
|
+
def read_components(self):
|
|
407
|
+
start_reg, c_fmt, _is_holding = self.address['tc_components']
|
|
408
|
+
start_reg += 14 # skipping timecycles...
|
|
409
|
+
values = self._read_reg_multi(start_reg, c_fmt, self.n_components, _is_holding)
|
|
410
|
+
|
|
411
|
+
return dict(zip(self.read_component_names(), values))
|
|
412
|
+
|
|
413
|
+
def read_ame_version(self):
|
|
414
|
+
start_reg, c_fmt, _is_holding = self.address['version_major']
|
|
415
|
+
major, minor, patch = self._read_reg_multi(start_reg, c_fmt, 3, _is_holding)
|
|
416
|
+
|
|
417
|
+
return f"{major}.{minor}.{patch}"
|
|
418
|
+
|
|
419
|
+
def read_ame_alarms(self):
|
|
420
|
+
start_reg, c_fmt, _is_holding = self.address['ame_alarms']
|
|
421
|
+
n_alarms = int(self._read_reg(start_reg, c_fmt, _is_holding))
|
|
422
|
+
values = self._read_reg_multi(start_reg + 2, c_fmt, n_alarms, _is_holding)
|
|
423
|
+
|
|
424
|
+
return dict(zip(self.read_component_names(), map(bool, values)))
|
|
425
|
+
|
|
426
|
+
def read_ame_timecycle(self):
|
|
427
|
+
return self.read_timecycle(kind='components')
|
|
428
|
+
|
|
429
|
+
_ame_parameter = namedtuple('ame_parameter', [
|
|
430
|
+
'step_number',
|
|
431
|
+
'run_number',
|
|
432
|
+
'use_mean',
|
|
433
|
+
'action_number',
|
|
434
|
+
'user_number'
|
|
435
|
+
])
|
|
436
|
+
|
|
437
|
+
def read_ame_numbers(self):
|
|
438
|
+
return IoniconModbus._ame_parameter(
|
|
439
|
+
int(self._read_reg(*self.address['step_number'])),
|
|
440
|
+
int(self._read_reg(*self.address['run_number'])),
|
|
441
|
+
int(self._read_reg(*self.address['use_mean'])),
|
|
442
|
+
int(self._read_reg(*self.address['action_number'])),
|
|
443
|
+
int(self._read_reg(*self.address['user_number'])),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
_ame_mean = namedtuple('ame_mean_info', [
|
|
447
|
+
'data_ok',
|
|
448
|
+
'mod_time',
|
|
449
|
+
'start_cycle',
|
|
450
|
+
'stop_cycle',
|
|
451
|
+
'mean_values',
|
|
452
|
+
'std_error',
|
|
453
|
+
])
|
|
454
|
+
|
|
455
|
+
def write_ame_action(self, action_number):
|
|
456
|
+
self.write_instrument_data(596, action_number, timeout_s=10)
|
|
457
|
+
|
|
458
|
+
def read_ame_mean(self, step_number=None):
|
|
459
|
+
start_reg, c_fmt, _is_holding = self.address['ame_mean_data']
|
|
460
|
+
data_ok = int(self._read_reg(start_reg, c_fmt, _is_holding))
|
|
461
|
+
if not data_ok:
|
|
462
|
+
return IoniconModbus._ame_mean(data_ok, 0, 0, 0, [], [])
|
|
463
|
+
|
|
464
|
+
n_masses = int(self._read_reg(start_reg + 6, c_fmt, _is_holding))
|
|
465
|
+
n_steps = int(self._read_reg(start_reg + 8, c_fmt, _is_holding))
|
|
466
|
+
|
|
467
|
+
if step_number is None:
|
|
468
|
+
step_number = int(self._read_reg(*self.address['step_number']))
|
|
469
|
+
elif not (0 < step_number <= n_steps):
|
|
470
|
+
raise IndexError(f"step_number [{step_number}] out of bounds: 0 < step <= [{n_steps}]")
|
|
471
|
+
|
|
472
|
+
datablock_size = 1 + 1 + n_masses + n_masses
|
|
473
|
+
start_addr = (start_reg
|
|
474
|
+
+ 10
|
|
475
|
+
+ n_masses * 2 # skip the masses, same as everywhere
|
|
476
|
+
+ (step_number-1) * datablock_size * 2) # select datablock
|
|
477
|
+
|
|
478
|
+
return IoniconModbus._ame_mean(
|
|
479
|
+
data_ok,
|
|
480
|
+
self._read_reg(*self.address['n_ame_mean']),
|
|
481
|
+
self._read_reg(start_addr + 0, c_fmt, _is_holding),
|
|
482
|
+
self._read_reg(start_addr + 2, c_fmt, _is_holding),
|
|
483
|
+
self._read_reg_multi(start_addr + 4, c_fmt, n_masses, _is_holding),
|
|
484
|
+
self._read_reg_multi(start_addr + 4 + n_masses * 2, c_fmt, n_masses, _is_holding),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def _read_reg(self, addr, c_format, is_holding_register=False):
|
|
488
|
+
n_bytes, c_format, reg_format = _get_fmt(c_format)
|
|
489
|
+
_read = self.mc.read_holding_registers if is_holding_register else self.mc.read_input_registers
|
|
490
|
+
|
|
491
|
+
register = _read(addr, n_bytes)
|
|
492
|
+
if register is None and _is_open(self.mc):
|
|
493
|
+
raise IOError(f"unable to read ({n_bytes}) registers at [{addr}] from connection")
|
|
494
|
+
elif register is None and not _is_open(self.mc):
|
|
495
|
+
raise IOError("trying to read from closed Modbus-connection")
|
|
496
|
+
|
|
497
|
+
return _unpack(register, c_format)
|
|
498
|
+
|
|
499
|
+
def _read_reg_multi(self, addr, c_format, n_values, is_holding_register=False):
|
|
500
|
+
rv = []
|
|
501
|
+
if not n_values > 0:
|
|
502
|
+
return rv
|
|
503
|
+
|
|
504
|
+
_read = self.mc.read_holding_registers if is_holding_register else self.mc.read_input_registers
|
|
505
|
+
|
|
506
|
+
n_bytes, c_format, reg_format = _get_fmt(c_format)
|
|
507
|
+
n_regs = n_bytes * n_values
|
|
508
|
+
|
|
509
|
+
# Note: there seems to be a limitation of modbus that
|
|
510
|
+
# the limits the number of registers to 125, so we
|
|
511
|
+
# read input-registers in blocks of 120:
|
|
512
|
+
blocks = ((addr + block, min(120, n_regs - block))
|
|
513
|
+
for block in range(0, n_regs, 120))
|
|
514
|
+
|
|
515
|
+
for block in blocks:
|
|
516
|
+
register = _read(*block)
|
|
517
|
+
if register is None and self.is_connected:
|
|
518
|
+
raise IOError(f"unable to read ({block[1]}) registers at [{block[0]}] from connection")
|
|
519
|
+
elif register is None and not self.is_connected:
|
|
520
|
+
raise IOError("trying to read from closed Modbus-connection")
|
|
521
|
+
|
|
522
|
+
# group the register-values by n_bytes, e.g. [1,2,3,4,..] ~> [(1,2),(3,4),..]
|
|
523
|
+
# this is a trick from the itertools-recipes, see
|
|
524
|
+
# https://docs.python.org/3.8/library/itertools.html?highlight=itertools#itertools-recipes
|
|
525
|
+
# note, that the iterator is cloned n-times and therefore
|
|
526
|
+
# all clones advance in parallel and can be zipped below:
|
|
527
|
+
batches = [iter(register)] * n_bytes
|
|
528
|
+
|
|
529
|
+
rv += [_unpack(reg, c_format) for reg in zip(*batches)]
|
|
530
|
+
|
|
531
|
+
return rv
|
|
532
|
+
|