pytrms 0.9.0__py3-none-any.whl → 0.9.1__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 +1 -2
- pytrms/clients/db_api.py +76 -84
- pytrms/clients/ioniclient.py +5 -21
- pytrms/clients/modbus.py +44 -11
- pytrms/clients/mqtt.py +34 -14
- pytrms/clients/ssevent.py +47 -52
- pytrms/compose/composition.py +118 -23
- pytrms/helpers.py +117 -3
- pytrms/peaktable.py +7 -5
- pytrms/readers/ionitof_reader.py +1 -10
- {pytrms-0.9.0.dist-info → pytrms-0.9.1.dist-info}/METADATA +2 -2
- pytrms-0.9.1.dist-info/RECORD +27 -0
- pytrms/clients/dirigent.py +0 -169
- pytrms/tracebuffer.py +0 -108
- pytrms-0.9.0.dist-info/RECORD +0 -29
- {pytrms-0.9.0.dist-info → pytrms-0.9.1.dist-info}/LICENSE +0 -0
- {pytrms-0.9.0.dist-info → pytrms-0.9.1.dist-info}/WHEEL +0 -0
pytrms/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
_version = '0.9.
|
|
1
|
+
_version = '0.9.1'
|
|
2
2
|
|
|
3
3
|
__all__ = ['load', 'connect']
|
|
4
4
|
|
|
@@ -23,7 +23,6 @@ def connect(host=None, method='webapi'):
|
|
|
23
23
|
returns an `Instrument` if connected successfully.
|
|
24
24
|
'''
|
|
25
25
|
from .instrument import Instrument
|
|
26
|
-
from .helpers import PTRConnectionError
|
|
27
26
|
|
|
28
27
|
if method.lower() == 'webapi':
|
|
29
28
|
from .clients.ioniclient import IoniClient
|
pytrms/clients/db_api.py
CHANGED
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import requests
|
|
5
5
|
|
|
6
6
|
from . import _logging
|
|
7
|
+
from .ssevent import SSEventListener
|
|
7
8
|
from .._base import IoniClientBase
|
|
8
9
|
|
|
9
10
|
log = _logging.getLogger(__name__)
|
|
@@ -82,7 +83,7 @@ class IoniConnect(IoniClientBase):
|
|
|
82
83
|
if not endpoint.startswith('/'):
|
|
83
84
|
endpoint = '/' + endpoint
|
|
84
85
|
if not isinstance(data, str):
|
|
85
|
-
data = json.dumps(data)
|
|
86
|
+
data = json.dumps(data, ensure_ascii=False) # default is `True`, escapes Umlaute!
|
|
86
87
|
if 'headers' not in kwargs:
|
|
87
88
|
kwargs['headers'] = {'content-type': 'application/hal+json'}
|
|
88
89
|
elif 'content-type' not in (k.lower() for k in kwargs['headers']):
|
|
@@ -94,93 +95,84 @@ class IoniConnect(IoniClientBase):
|
|
|
94
95
|
|
|
95
96
|
return r
|
|
96
97
|
|
|
97
|
-
def
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for component in j["_embedded"]["components"]}
|
|
120
|
-
|
|
121
|
-
def get_component(self, short_name):
|
|
122
|
-
if not len(self.comp_dict):
|
|
123
|
-
self.refresh_comp_dict()
|
|
124
|
-
|
|
125
|
-
return self.comp_dict[short_name]
|
|
126
|
-
|
|
127
|
-
def create_component(self, short_name):
|
|
128
|
-
payload = {
|
|
129
|
-
"shortName": short_name
|
|
98
|
+
def sync(self, peaktable):
|
|
99
|
+
"""Compare and upload any differences in `peaktable` to the database."""
|
|
100
|
+
from pytrms.peaktable import Peak, PeakTable
|
|
101
|
+
from operator import attrgetter
|
|
102
|
+
|
|
103
|
+
# Note: a `Peak` is a hashable object that serves as a key that
|
|
104
|
+
# distinguishes between peaks as defined by PyTRMS:
|
|
105
|
+
make_key = lambda peak: Peak(center=peak['center'], label=peak['name'], shift=peak['shift'])
|
|
106
|
+
|
|
107
|
+
if isinstance(peaktable, str):
|
|
108
|
+
log.info(f"loading peaktable '{peaktable}'...")
|
|
109
|
+
peaktable = PeakTable.from_file(peaktable)
|
|
110
|
+
|
|
111
|
+
# get the PyTRMS- and IoniConnect-peaks on the same page:
|
|
112
|
+
conv = {
|
|
113
|
+
'name': attrgetter('label'),
|
|
114
|
+
'center': attrgetter('center'),
|
|
115
|
+
'kRate': attrgetter('k_rate'),
|
|
116
|
+
'low': lambda p: p.borders[0],
|
|
117
|
+
'high': lambda p: p.borders[1],
|
|
118
|
+
'shift': attrgetter('shift'),
|
|
119
|
+
'multiplier': attrgetter('multiplier'),
|
|
130
120
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
121
|
+
# normalize the input argument and create a hashable set:
|
|
122
|
+
updates = dict()
|
|
123
|
+
for peak in peaktable:
|
|
124
|
+
payload = {k: conv[k](peak) for k in conv}
|
|
125
|
+
updates[make_key(payload)] = {'payload': payload}
|
|
126
|
+
|
|
127
|
+
log.info(f"fetching current peaktable from the server...")
|
|
128
|
+
# create a comparable collection of peaks already on the database by
|
|
129
|
+
# reducing the keys in the response to what we actually want to update:
|
|
130
|
+
db_peaks = {make_key(p): {
|
|
131
|
+
'payload': {k: p[k] for k in conv.keys()},
|
|
132
|
+
'self': p['_links']['self'],
|
|
133
|
+
'parent': p['_links'].get('parent'),
|
|
134
|
+
} for p in self.get('/api/peaks')['_embedded']['peaks']}
|
|
135
|
+
|
|
136
|
+
to_update = updates.keys() & db_peaks.keys()
|
|
137
|
+
to_upload = updates.keys() - db_peaks.keys()
|
|
138
|
+
updated = 0
|
|
139
|
+
for key in sorted(to_update):
|
|
140
|
+
# check if an existing peak needs an update
|
|
141
|
+
if db_peaks[key]['payload'] == updates[key]['payload']:
|
|
142
|
+
# nothing to do..
|
|
143
|
+
log.debug(f"up-to-date: {key}")
|
|
144
|
+
continue
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
146
|
+
self.put(db_peaks[key]['self']['href'], updates[key]['payload'])
|
|
147
|
+
log.info(f"updated: {key}")
|
|
148
|
+
updated += 1
|
|
149
|
+
|
|
150
|
+
if len(to_upload):
|
|
151
|
+
# Note: POSTing the embedded-collection is *miles faster*
|
|
152
|
+
# than doing separate requests for each peak!
|
|
153
|
+
payload = {'_embedded': {'peaks': [updates[key]['payload'] for key in sorted(to_upload)]}}
|
|
154
|
+
self.post('/api/peaks', payload)
|
|
155
|
+
for key in sorted(to_upload): log.info(f"added new: {key}")
|
|
156
|
+
|
|
157
|
+
# Note: this disregards the peak-parent-relationship, but in
|
|
158
|
+
# order to implement this correctly, one would need to check
|
|
159
|
+
# if the parent-peak with a specific 'parentID' is already
|
|
160
|
+
# uploaded and search it.. there's an endpoint
|
|
161
|
+
# 'LINK /api/peaks/{parentID} Location: /api/peaks/{childID}'
|
|
162
|
+
# to link a child to its parent, but it remains complicated.
|
|
163
|
+
# TODO :: maybe later implement parent-peaks!?
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
'added': len(to_upload),
|
|
167
|
+
'updated': updated,
|
|
168
|
+
'up-to-date': len(to_update) - updated,
|
|
159
169
|
}
|
|
160
|
-
endpoint = self.current_avg_endpoint + '/component_traces'
|
|
161
|
-
self.put(endpoint, payload)
|
|
162
170
|
|
|
163
|
-
def
|
|
164
|
-
"""
|
|
165
|
-
Post Parameters to the database.
|
|
171
|
+
def iter_events(self, event_re=r".*"):
|
|
172
|
+
"""Follow the server-sent-events (SSE) on the DB-API.
|
|
166
173
|
|
|
167
|
-
`
|
|
174
|
+
`event_re` a regular expression to filter events (default: matches everything)
|
|
168
175
|
"""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# this expects a namedtuple as defined in Modbus client: .set, .act, .par_id
|
|
172
|
-
if self.current_avg_endpoint is None:
|
|
173
|
-
raise Exception("create average first")
|
|
174
|
-
|
|
175
|
-
payload = {
|
|
176
|
-
"quantities": [
|
|
177
|
-
{
|
|
178
|
-
"parameterID": item.par_id,
|
|
179
|
-
"setValue": item.set,
|
|
180
|
-
"actMean": item.act
|
|
181
|
-
} for name, item in new_values.items()
|
|
182
|
-
]
|
|
183
|
-
}
|
|
184
|
-
endpoint = self.current_avg_endpoint + '/parameter_traces' # on the DB it's called parameter... :\
|
|
185
|
-
self.put(endpoint, payload)
|
|
176
|
+
yield from SSEventListener(event_re, host_url=self.url, endpoint="/api/events",
|
|
177
|
+
session=self.session)
|
|
186
178
|
|
pytrms/clients/ioniclient.py
CHANGED
|
@@ -18,17 +18,17 @@ class IoniClient:
|
|
|
18
18
|
Access the Ionicon WebAPI.
|
|
19
19
|
|
|
20
20
|
Usage:
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
> client = IoniClient()
|
|
22
|
+
> client.get('TPS_Pull_H')
|
|
23
23
|
{'TPS_Pull_H': 123.45, ... }
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
> client.set('TPS_Pull_H', 42)
|
|
26
26
|
{'TPS_Pull_H': 42.0, ... }
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
> client.start_measurement()
|
|
29
29
|
ACK
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
> client.host, client.port
|
|
32
32
|
('localhost', 8002)
|
|
33
33
|
|
|
34
34
|
'''
|
|
@@ -85,19 +85,3 @@ class IoniClient:
|
|
|
85
85
|
|
|
86
86
|
def stop_measurement(self):
|
|
87
87
|
return self.set('ACQ_SRV_Stop_Meas', 1)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if __name__ == '__main__':
|
|
91
|
-
import sys
|
|
92
|
-
client = IoniClient()
|
|
93
|
-
|
|
94
|
-
if len(sys.argv) == 2:
|
|
95
|
-
print(client.get(sys.argv[1]))
|
|
96
|
-
elif len(sys.argv) == 3:
|
|
97
|
-
print(client.set(sys.argv[1], sys.argv[2]))
|
|
98
|
-
else:
|
|
99
|
-
print(f"""\
|
|
100
|
-
usage:
|
|
101
|
-
python {sys.argv[0]} <varname> [<value>]
|
|
102
|
-
""")
|
|
103
|
-
|
pytrms/clients/modbus.py
CHANGED
|
@@ -9,7 +9,7 @@ from collections import namedtuple
|
|
|
9
9
|
from functools import lru_cache
|
|
10
10
|
from itertools import tee
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import pyModbusTCP.client
|
|
13
13
|
|
|
14
14
|
from . import _par_id_file
|
|
15
15
|
from .._base.ioniclient import IoniClientBase
|
|
@@ -19,9 +19,28 @@ log = logging.getLogger(__name__)
|
|
|
19
19
|
__all__ = ['IoniconModbus']
|
|
20
20
|
|
|
21
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
|
+
|
|
22
39
|
with open(_par_id_file) as f:
|
|
23
40
|
it = iter(f)
|
|
24
|
-
assert next(it).startswith('ID\tName'), "Modbus parameter file is corrupt: "
|
|
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!")
|
|
25
44
|
_id_to_descr = {int(id_): name for id_, name, *_ in (line.strip().split('\t') for line in it)}
|
|
26
45
|
|
|
27
46
|
# look-up-table for c_structs (see docstring of struct-module for more info).
|
|
@@ -59,10 +78,10 @@ def _unpack(registers, format='>f'):
|
|
|
59
78
|
representation, respectively.
|
|
60
79
|
|
|
61
80
|
>>> _unpack([17448, 0], 'float')
|
|
62
|
-
672.
|
|
81
|
+
672.0
|
|
63
82
|
|
|
64
83
|
>>> _unpack([17446, 32768], 'float')
|
|
65
|
-
666.
|
|
84
|
+
666.0
|
|
66
85
|
|
|
67
86
|
>>> _unpack([16875, 61191, 54426, 37896], 'double')
|
|
68
87
|
3749199524.83057
|
|
@@ -132,7 +151,7 @@ class IoniconModbus(IoniClientBase):
|
|
|
132
151
|
|
|
133
152
|
@property
|
|
134
153
|
def is_connected(self):
|
|
135
|
-
if not self.mc
|
|
154
|
+
if not _is_open(self.mc):
|
|
136
155
|
return False
|
|
137
156
|
|
|
138
157
|
# wait for the IoniTOF alive-counter to change (1 second max)...
|
|
@@ -175,7 +194,18 @@ class IoniconModbus(IoniClientBase):
|
|
|
175
194
|
|
|
176
195
|
def __init__(self, host='localhost', port=502):
|
|
177
196
|
super().__init__(host, port)
|
|
178
|
-
|
|
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
|
+
)
|
|
179
209
|
# try connect immediately:
|
|
180
210
|
try:
|
|
181
211
|
self.connect()
|
|
@@ -185,8 +215,11 @@ class IoniconModbus(IoniClientBase):
|
|
|
185
215
|
|
|
186
216
|
def connect(self, timeout_s=10):
|
|
187
217
|
log.info(f"[{self}] connecting to Modbus server...")
|
|
188
|
-
|
|
189
|
-
self.mc.
|
|
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
|
|
190
223
|
if not self.mc.open():
|
|
191
224
|
raise TimeoutError(f"[{self}] no connection to modbus socket")
|
|
192
225
|
|
|
@@ -201,7 +234,7 @@ class IoniconModbus(IoniClientBase):
|
|
|
201
234
|
raise TimeoutError(f"[{self}] no connection to IoniTOF");
|
|
202
235
|
|
|
203
236
|
def disconnect(self):
|
|
204
|
-
if self.mc
|
|
237
|
+
if _is_open(self.mc):
|
|
205
238
|
self.mc.close()
|
|
206
239
|
|
|
207
240
|
@property
|
|
@@ -452,9 +485,9 @@ class IoniconModbus(IoniClientBase):
|
|
|
452
485
|
_read = self.mc.read_holding_registers if is_holding_register else self.mc.read_input_registers
|
|
453
486
|
|
|
454
487
|
register = _read(addr, n_bytes)
|
|
455
|
-
if register is None and self.mc
|
|
488
|
+
if register is None and _is_open(self.mc):
|
|
456
489
|
raise IOError(f"unable to read ({n_bytes}) registers at [{addr}] from connection")
|
|
457
|
-
elif register is None and not self.mc
|
|
490
|
+
elif register is None and not _is_open(self.mc):
|
|
458
491
|
raise IOError("trying to read from closed Modbus-connection")
|
|
459
492
|
|
|
460
493
|
return _unpack(register, c_format)
|
pytrms/clients/mqtt.py
CHANGED
|
@@ -325,9 +325,14 @@ def follow_state(client, self, msg):
|
|
|
325
325
|
log.debug(f"[{self}] new server-state: " + str(state))
|
|
326
326
|
# replace the current state with the new element:
|
|
327
327
|
self._server_state.append(state)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
meas_running = (state == "ACQ_Aquire") # yes, there's a typo, plz keep it :)
|
|
329
|
+
just_started = (meas_running and not msg.retain)
|
|
330
|
+
if meas_running:
|
|
331
|
+
# signal the relevant thread(s) that we need an update:
|
|
332
|
+
self._calcconzinfo.append(_NOT_INIT)
|
|
333
|
+
if just_started:
|
|
334
|
+
# invalidate the source-file until we get a new one:
|
|
335
|
+
self._sf_filename.append(_NOT_INIT)
|
|
331
336
|
|
|
332
337
|
follow_state.topics = ["DataCollection/Act/ACQ_SRV_CurrentState"]
|
|
333
338
|
|
|
@@ -404,8 +409,8 @@ _NOT_INIT = object()
|
|
|
404
409
|
class MqttClient(MqttClientBase):
|
|
405
410
|
"""a simplified client for the Ionicon MQTT API.
|
|
406
411
|
|
|
407
|
-
|
|
408
|
-
|
|
412
|
+
> mq = MqttClient()
|
|
413
|
+
> mq.write('TCP_MCP_B', 3400)
|
|
409
414
|
ValueError()
|
|
410
415
|
|
|
411
416
|
"""
|
|
@@ -463,11 +468,27 @@ class MqttClient(MqttClientBase):
|
|
|
463
468
|
|
|
464
469
|
@property
|
|
465
470
|
def current_sourcefile(self):
|
|
466
|
-
'''Returns the path to the hdf5-file that is currently
|
|
471
|
+
'''Returns the path to the hdf5-file that is currently being written.
|
|
467
472
|
|
|
468
|
-
|
|
473
|
+
Returns an empty string if no measurement is running.
|
|
469
474
|
'''
|
|
470
|
-
|
|
475
|
+
if not self.is_running:
|
|
476
|
+
return ""
|
|
477
|
+
|
|
478
|
+
if self._sf_filename[0] is not _NOT_INIT:
|
|
479
|
+
return self._sf_filename[0]
|
|
480
|
+
|
|
481
|
+
# Note: '_NOT_INIT' is set by us on start of acquisition, so we'd expect
|
|
482
|
+
# to receive the source-file-topic after a (generous) timeout:
|
|
483
|
+
timeout_s = 15
|
|
484
|
+
started_at = time.monotonic()
|
|
485
|
+
while time.monotonic() < started_at + timeout_s:
|
|
486
|
+
if self._sf_filename[0] is not _NOT_INIT:
|
|
487
|
+
return self._sf_filename[0]
|
|
488
|
+
|
|
489
|
+
time.sleep(10e-3)
|
|
490
|
+
else:
|
|
491
|
+
raise TimeoutError(f"[{self}] unable to retrieve source-file after ({timeout_s = })");
|
|
471
492
|
|
|
472
493
|
@property
|
|
473
494
|
def current_cycle(self):
|
|
@@ -508,12 +529,10 @@ class MqttClient(MqttClientBase):
|
|
|
508
529
|
if _lut is self.set_values and is_read_only:
|
|
509
530
|
raise ValueError(f"'{parID}' is read-only, did you mean `kind='act'`?")
|
|
510
531
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
return _lut[parID]
|
|
516
|
-
except KeyError as exc:
|
|
532
|
+
if not parID in _lut:
|
|
533
|
+
# Note: The values should need NO! time to be populated from the MQTT topics,
|
|
534
|
+
# because all topics are published as *retained* by the PTR-server.
|
|
535
|
+
# However, a short timeout is respected before raising a `KeyError`:
|
|
517
536
|
time.sleep(200e-3)
|
|
518
537
|
rv = _lut.get(parID)
|
|
519
538
|
if rv is not None:
|
|
@@ -525,6 +544,7 @@ class MqttClient(MqttClientBase):
|
|
|
525
544
|
"set" if parID in self.set_values else
|
|
526
545
|
"")
|
|
527
546
|
raise KeyError(str(parID) + (' (did you mean `kind="%s"`?)' % error_hint) if error_hint else "")
|
|
547
|
+
return _lut[parID]
|
|
528
548
|
|
|
529
549
|
def get_table(self, table_name):
|
|
530
550
|
timeout_s = 10
|
pytrms/clients/ssevent.py
CHANGED
|
@@ -1,48 +1,19 @@
|
|
|
1
1
|
import re
|
|
2
|
-
import
|
|
2
|
+
from collections import namedtuple
|
|
3
3
|
from collections.abc import Iterable
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
|
|
7
|
-
from . import
|
|
7
|
+
from . import _logging
|
|
8
8
|
|
|
9
|
-
log =
|
|
9
|
+
log = _logging.getLogger()
|
|
10
10
|
|
|
11
|
+
_event_rv = namedtuple('ssevent', ['event', 'data'])
|
|
11
12
|
|
|
12
13
|
class SSEventListener(Iterable):
|
|
13
14
|
|
|
14
|
-
def __init__(self, endpoint=''):
|
|
15
|
-
if not endpoint:
|
|
16
|
-
endpoint = ionitof_url + '/api/timing/stream'
|
|
17
|
-
self.endpoint = endpoint
|
|
18
|
-
self._response = None
|
|
19
|
-
self.subscriptions = set()
|
|
20
|
-
|
|
21
|
-
def subscribe(self, event='cycle'):
|
|
22
|
-
"""Listen for events matching the given string or regular expression.
|
|
23
|
-
|
|
24
|
-
This will already match if the string is contained in the event-topic.
|
|
25
|
-
"""
|
|
26
|
-
if self._response is None:
|
|
27
|
-
r = requests.get(self.endpoint, headers={'accept': 'text/event-stream'}, stream=True)
|
|
28
|
-
if not r.ok:
|
|
29
|
-
log.error(f"no connection to {self.endpoint} (got [{r.status_code}])")
|
|
30
|
-
r.raise_for_status()
|
|
31
|
-
|
|
32
|
-
self._response = r
|
|
33
|
-
|
|
34
|
-
regex = re.compile('.*' + event + '.*') # be rather loose with matching..
|
|
35
|
-
self.subscriptions.add(regex)
|
|
36
|
-
|
|
37
|
-
def unsubscribe(self, event='cycle'):
|
|
38
|
-
self.subscriptions.remove(event)
|
|
39
|
-
if not len(self.subscriptions):
|
|
40
|
-
log.debug(f"closing connection to {self.endpoint}")
|
|
41
|
-
self._response.close()
|
|
42
|
-
self._response = None
|
|
43
|
-
|
|
44
15
|
@staticmethod
|
|
45
|
-
def
|
|
16
|
+
def _line_stream(response):
|
|
46
17
|
# Note: using .iter_content() seems to yield results faster than .iter_lines()
|
|
47
18
|
line = ''
|
|
48
19
|
for bite in response.iter_content(chunk_size=1, decode_unicode=True):
|
|
@@ -51,32 +22,56 @@ class SSEventListener(Iterable):
|
|
|
51
22
|
yield line
|
|
52
23
|
line = ''
|
|
53
24
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
25
|
+
def __init__(self, event_re=None, host_url='http://127.0.0.1:5066',
|
|
26
|
+
endpoint='/api/events', session=None):
|
|
27
|
+
self.uri = host_url + endpoint
|
|
28
|
+
if session is not None:
|
|
29
|
+
self._get = session.get
|
|
30
|
+
else:
|
|
31
|
+
self._get = requests.get
|
|
32
|
+
self._connect_response = None
|
|
33
|
+
self.subscriptions = set()
|
|
34
|
+
if event_re is not None:
|
|
35
|
+
self.subscribe(event_re)
|
|
36
|
+
|
|
37
|
+
def subscribe(self, event_re):
|
|
38
|
+
"""Listen for events matching the given string or regular expression."""
|
|
39
|
+
self.subscriptions.add(re.compile(event_re))
|
|
40
|
+
if self._connect_response is None:
|
|
41
|
+
r = self._get(self.uri, headers={'accept': 'text/event-stream'}, stream=True)
|
|
42
|
+
if not r.ok:
|
|
43
|
+
log.error(f"no connection to {self.uri} (got [{r.status_code}])")
|
|
44
|
+
r.raise_for_status()
|
|
45
|
+
|
|
46
|
+
self._connect_response = r
|
|
47
|
+
|
|
48
|
+
def unsubscribe(self, event_re):
|
|
49
|
+
"""Stop listening for certain events."""
|
|
50
|
+
self.subscriptions.remove(re.compile(event_re))
|
|
51
|
+
if not len(self.subscriptions):
|
|
52
|
+
log.debug(f"closing connection to {self.uri}")
|
|
53
|
+
self._connect_response.close()
|
|
54
|
+
self._connect_response = None
|
|
57
55
|
|
|
58
56
|
def __iter__(self):
|
|
59
|
-
if self.
|
|
57
|
+
if self._connect_response is None:
|
|
60
58
|
raise Exception("call .subscribe() first to listen for events")
|
|
61
59
|
|
|
62
|
-
|
|
60
|
+
event = msg = ''
|
|
61
|
+
for line in self._line_stream(self._connect_response): # blocks...
|
|
63
62
|
if not line.strip():
|
|
63
|
+
if event and any(re.match(sub, event) for sub in self.subscriptions):
|
|
64
|
+
yield _event_rv(event, msg)
|
|
65
|
+
event = msg = ''
|
|
66
|
+
else:
|
|
67
|
+
log.log(_logging.TRACE, "sse: still alive...")
|
|
64
68
|
continue
|
|
65
69
|
|
|
66
|
-
key,
|
|
67
|
-
msg = msg.strip()
|
|
70
|
+
key, val = line.split(':', maxsplit=1)
|
|
68
71
|
if key == 'event':
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if not any(sub.match(self.event) for sub in self.subscriptions):
|
|
72
|
-
log.debug(f"skipping event <{msg}>")
|
|
73
|
-
continue
|
|
74
|
-
|
|
72
|
+
event = val.strip()
|
|
75
73
|
elif key == 'data':
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
msg += val.strip()
|
|
78
75
|
else:
|
|
79
|
-
log.warning(f"
|
|
80
|
-
continue
|
|
81
|
-
|
|
76
|
+
log.warning(f"unknown SSE-key <{key}> in stream")
|
|
82
77
|
|
pytrms/compose/composition.py
CHANGED
|
@@ -4,6 +4,7 @@ import itertools
|
|
|
4
4
|
import logging
|
|
5
5
|
from collections.abc import Iterable
|
|
6
6
|
from collections import namedtuple
|
|
7
|
+
from itertools import tee
|
|
7
8
|
from functools import wraps
|
|
8
9
|
|
|
9
10
|
__all__ = ['Step', 'Composition']
|
|
@@ -30,14 +31,19 @@ class Step:
|
|
|
30
31
|
>>> step.set_values
|
|
31
32
|
{'DPS_Udrift': 500}
|
|
32
33
|
|
|
33
|
-
Note, that no Automation-numbers can be defined in the step
|
|
34
|
+
Note, that no Automation-numbers can be defined in the step..
|
|
34
35
|
>>> Step("H50", {'AUTO_UseMean': 0}, 10, start_delay=2)
|
|
35
36
|
Traceback (most recent call last):
|
|
36
37
|
...
|
|
37
38
|
AssertionError: Automation numbers cannot be defined
|
|
38
39
|
|
|
39
|
-
''
|
|
40
|
+
..and neither can a 'OP_Mode' alongside anything else:
|
|
41
|
+
>>> Step("Odd2", {'DPS_Udrift': 345, 'OP_Mode': 2}, 10, start_delay=2)
|
|
42
|
+
Traceback (most recent call last):
|
|
43
|
+
...
|
|
44
|
+
AssertionError: if 'OP_Mode' is specified, nothing else can be
|
|
40
45
|
|
|
46
|
+
'''
|
|
41
47
|
protected_keys = ['AME_RunNumber', 'AME_StepNumber', 'AUTO_UseMean']
|
|
42
48
|
|
|
43
49
|
def __init__(self, name, set_values, duration, start_delay):
|
|
@@ -51,8 +57,13 @@ class Step:
|
|
|
51
57
|
assert self.start_delay >= 0
|
|
52
58
|
assert self.start_delay < self.duration
|
|
53
59
|
|
|
54
|
-
for key in self.set_values
|
|
60
|
+
for key in self.set_values:
|
|
55
61
|
assert key not in Step.protected_keys, "Automation numbers cannot be defined"
|
|
62
|
+
if 'OP_Mode' in self.set_values:
|
|
63
|
+
assert len(self.set_values) == 1, "if 'OP_Mode' is specified, nothing else can be"
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return f"{self.name}: ({self.start_delay}/{self.duration}) sec ~> {self.set_values}"
|
|
56
67
|
|
|
57
68
|
|
|
58
69
|
class Composition(Iterable):
|
|
@@ -77,15 +88,34 @@ class Composition(Iterable):
|
|
|
77
88
|
>>> list(co.sequence())
|
|
78
89
|
[(8, {'Eins': 1}), (18, {'Zwei': 2})]
|
|
79
90
|
|
|
80
|
-
...with an action-number at the start...
|
|
91
|
+
...with an action-number at the start (note, that AME-numbers are 1 cycle ahead)...
|
|
81
92
|
>>> co.start_action = 7
|
|
82
93
|
>>> list(co.sequence())
|
|
83
|
-
[(
|
|
94
|
+
[(9, {'AME_ActionNumber': 7}), (8, {'Eins': 1}), (18, {'Zwei': 2})]
|
|
84
95
|
|
|
85
96
|
...or with automation numbers, where the 'start_delay' comes into play:
|
|
86
97
|
>>> co.generate_automation = True
|
|
87
|
-
>>>
|
|
88
|
-
|
|
98
|
+
>>> seq = co.sequence()
|
|
99
|
+
>>> next(seq)
|
|
100
|
+
(9, {'AME_ActionNumber': 7})
|
|
101
|
+
|
|
102
|
+
>>> next(seq)
|
|
103
|
+
(8, {'Eins': 1})
|
|
104
|
+
|
|
105
|
+
>>> next(seq)
|
|
106
|
+
(9, {'AME_StepNumber': 1, 'AME_RunNumber': 1, 'AUTO_UseMean': 0})
|
|
107
|
+
|
|
108
|
+
>>> next(seq)
|
|
109
|
+
(11, {'AUTO_UseMean': 1})
|
|
110
|
+
|
|
111
|
+
>>> next(seq)
|
|
112
|
+
(18, {'Zwei': 2})
|
|
113
|
+
|
|
114
|
+
>>> next(seq)
|
|
115
|
+
(19, {'AME_StepNumber': 2, 'AUTO_UseMean': 0})
|
|
116
|
+
|
|
117
|
+
>>> next(seq)
|
|
118
|
+
(22, {'AUTO_UseMean': 1})
|
|
89
119
|
|
|
90
120
|
'''
|
|
91
121
|
|
|
@@ -122,6 +152,67 @@ class Composition(Iterable):
|
|
|
122
152
|
def dump(self, ofstream):
|
|
123
153
|
json.dump(self, ofstream, indent=2, default=vars)
|
|
124
154
|
|
|
155
|
+
def translate_op_modes(self, preset_items, check=True):
|
|
156
|
+
'''Given the `preset_items` (from a presets-file), compile a list of set_values.
|
|
157
|
+
|
|
158
|
+
>>> presets = {}
|
|
159
|
+
>>> presets[0] = ('H3O+', {('Drift', 'Global_System.DriftPressureSet', 'FLOAT'): 2.6})
|
|
160
|
+
>>> presets[2] = ('O3+', {('T-Drift[°C]', 'Global_Temperatures.TempsSet[0]', 'FLOAT'): 75.0})
|
|
161
|
+
|
|
162
|
+
next, define a couple of Steps that use the presets (a.k.a. 'OP_Mode'):
|
|
163
|
+
>>> steps = []
|
|
164
|
+
>>> steps.append(Step('uno', {'OP_Mode': 0}, 10, 2)) # set p-Drift by OP_Mode
|
|
165
|
+
>>> steps.append(Step('due', {'Udrift': 420.0, 'T-Drift': 81.0}, 10, 2))
|
|
166
|
+
>>> steps.append(Step('tre', {'OP_Mode': 2}, 10, 2)) # set T-Drift by OP_Mode
|
|
167
|
+
|
|
168
|
+
the Composition of these steps will translate to the output underneath.
|
|
169
|
+
note, that the set-value for Pdrift_Ctrl is carried along with each step:
|
|
170
|
+
>>> co = Composition(steps)
|
|
171
|
+
>>> co.translate_op_modes(presets, check=False)
|
|
172
|
+
[{'DPS_Pdrift_Ctrl_Val': 2.6}, {'Udrift': 420.0, 'T-Drift': 81.0, 'DPS_Pdrift_Ctrl_Val': 2.6}, {'T-Drift': 75.0, 'DPS_Pdrift_Ctrl_Val': 2.6, 'Udrift': 420.0}]
|
|
173
|
+
|
|
174
|
+
Since we didn't specify the full set of reaction-parameters, the self-check will fail:
|
|
175
|
+
>>> co.translate_op_modes(presets, check=True)
|
|
176
|
+
Traceback (most recent call last):
|
|
177
|
+
...
|
|
178
|
+
AssertionError: reaction-data missing in presets
|
|
179
|
+
|
|
180
|
+
'''
|
|
181
|
+
if preset_items is None:
|
|
182
|
+
raise ValueError('preset_items is None')
|
|
183
|
+
|
|
184
|
+
# Note: the `preset_items` is a dict[step_index] ~> (name, preset_items)
|
|
185
|
+
# and in the items one would expect these keys:
|
|
186
|
+
preset_keys = {
|
|
187
|
+
'PrimionIdx': ('PrimionIdx', '', 'INT'),
|
|
188
|
+
'TransmissionIdx': ('TransmissionIdx', '', 'INT'),
|
|
189
|
+
'DPS_Udrift': ('UDrift', 'Global_DTS500.TR_DTS500_Set[0].SetU_Udrift', 'FLOAT'),
|
|
190
|
+
'DPS_Pdrift_Ctrl_Val': ('Drift', 'Global_System.DriftPressureSet', 'FLOAT'),
|
|
191
|
+
'T-Drift': ('T-Drift[°C]', 'Global_Temperatures.TempsSet[0]', 'FLOAT'),
|
|
192
|
+
}
|
|
193
|
+
# make a deep copy of the `set_values`:
|
|
194
|
+
set_values = [dict(step.set_values) for step in self.steps]
|
|
195
|
+
carry = dict()
|
|
196
|
+
for entry in set_values:
|
|
197
|
+
# replace OP_Mode with the stuff found in preset_items
|
|
198
|
+
if 'OP_Mode' in entry:
|
|
199
|
+
index = entry['OP_Mode']
|
|
200
|
+
name, items = preset_items[index]
|
|
201
|
+
for parID, key in preset_keys.items():
|
|
202
|
+
if key in items:
|
|
203
|
+
entry[parID] = items[key]
|
|
204
|
+
del entry['OP_Mode']
|
|
205
|
+
|
|
206
|
+
# Note: each preset is only an update of set-values over what has already
|
|
207
|
+
# been set. thus, when following the sequence of OP_Modes, each one must
|
|
208
|
+
# carry with it the set-values of all its predecessors:
|
|
209
|
+
carry.update(entry)
|
|
210
|
+
entry.update(carry)
|
|
211
|
+
if check:
|
|
212
|
+
assert all(key in entry for key in preset_keys), "reaction-data missing in presets"
|
|
213
|
+
|
|
214
|
+
return set_values
|
|
215
|
+
|
|
125
216
|
def sequence(self):
|
|
126
217
|
'''A (possibly infinite) iterator over this Composition's future_cycles and steps.
|
|
127
218
|
|
|
@@ -130,27 +221,31 @@ class Composition(Iterable):
|
|
|
130
221
|
The first 'future_cycle' is 0 unless otherwise specified with class-parameter 'start_cycle'.
|
|
131
222
|
This generates AME_Run/Step-Number and AUTO_UseMean unless otherwise specified.
|
|
132
223
|
'''
|
|
224
|
+
_offset_ame = True # whether ame-numbers should mark the *next* cycle, see [#2897]
|
|
225
|
+
|
|
133
226
|
future_cycle = self.start_cycle
|
|
134
227
|
if self.start_action is not None:
|
|
135
|
-
yield future_cycle, dict([(self.ACTION_MARKER, int(self.start_action))])
|
|
228
|
+
yield future_cycle + int(_offset_ame), dict([(self.ACTION_MARKER, int(self.start_action))])
|
|
136
229
|
|
|
137
|
-
for run, step,
|
|
138
|
-
|
|
139
|
-
if step == 1:
|
|
140
|
-
automation[self.RUN_MARKER] = run
|
|
230
|
+
for run, step, step_info in self:
|
|
231
|
+
yield future_cycle, dict(step_info.set_values)
|
|
141
232
|
|
|
142
|
-
set_values = dict(current.set_values)
|
|
143
233
|
if self.generate_automation:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
234
|
+
automation = {self.STEP_MARKER: step}
|
|
235
|
+
if step == 1:
|
|
236
|
+
automation[self.RUN_MARKER] = run
|
|
237
|
+
|
|
238
|
+
if step_info.start_delay == 0:
|
|
239
|
+
# all cycles get the AUTO_UseMean flag set to True:
|
|
240
|
+
automation[self.USE_MARKER] = 1
|
|
241
|
+
yield future_cycle + int(_offset_ame), automation
|
|
242
|
+
else:
|
|
243
|
+
# split into two updates for AUTO_UseMean flag:
|
|
244
|
+
automation[self.USE_MARKER] = 0
|
|
245
|
+
yield future_cycle + int(_offset_ame), automation
|
|
246
|
+
yield future_cycle + int(_offset_ame) + step_info.start_delay, {self.USE_MARKER: 1}
|
|
247
|
+
|
|
248
|
+
future_cycle = future_cycle + step_info.duration
|
|
154
249
|
|
|
155
250
|
@coroutine
|
|
156
251
|
def schedule_routine(self, schedule_fun):
|
pytrms/helpers.py
CHANGED
|
@@ -1,6 +1,120 @@
|
|
|
1
|
-
|
|
1
|
+
"""@file helpers.py
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
common helper functions.
|
|
4
|
+
"""
|
|
5
5
|
|
|
6
|
+
def convert_labview_to_posix(lv_time_utc, utc_offset_sec):
|
|
7
|
+
'''Create a `pandas.Timestamp` from LabView time.'''
|
|
8
|
+
from pandas import Timestamp
|
|
9
|
+
|
|
10
|
+
# change epoch from 01.01.1904 to 01.01.1970:
|
|
11
|
+
posix_time = lv_time_utc - 2082844800
|
|
12
|
+
# the tz must be specified in isoformat like '+02:30'..
|
|
13
|
+
tz_sec = int(utc_offset_sec)
|
|
14
|
+
tz_designator = '{0}{1:02d}:{2:02d}'.format(
|
|
15
|
+
'+' if tz_sec >= 0 else '-', tz_sec // 3600, tz_sec % 3600 // 60)
|
|
16
|
+
|
|
17
|
+
return Timestamp(posix_time, unit='s', tz=tz_designator)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_presets_file(presets_file):
|
|
21
|
+
'''Load a `presets_file` as XML-tree and interpret the "OP_Mode" of this `Composition`.
|
|
22
|
+
|
|
23
|
+
The tricky thing is, that any OP_Mode may or may not override previous settings!
|
|
24
|
+
Therefore, it depends on the order of modes in this Composition to be able to assign
|
|
25
|
+
each OP_Mode its actual dictionary of set_values.
|
|
26
|
+
|
|
27
|
+
Note, that the preset file uses its own naming convention that cannot neccessarily be
|
|
28
|
+
translated into standard parID-names. You may choose whatever you like to do with it.
|
|
29
|
+
'''
|
|
30
|
+
import xml.etree.ElementTree as ET
|
|
31
|
+
from collections import namedtuple, defaultdict
|
|
32
|
+
|
|
33
|
+
_key = namedtuple('preset_item', ['name', 'ads_path', 'dtype'])
|
|
34
|
+
_parse_value = {
|
|
35
|
+
"FLOAT": float,
|
|
36
|
+
"BOOL": bool,
|
|
37
|
+
"BYTE": int,
|
|
38
|
+
"ENUM": int,
|
|
39
|
+
}
|
|
40
|
+
tree = ET.parse(presets_file)
|
|
41
|
+
root = tree.getroot()
|
|
42
|
+
|
|
43
|
+
preset_names = {}
|
|
44
|
+
preset_items = defaultdict(dict)
|
|
45
|
+
for index, preset in enumerate(root.iterfind('preset')):
|
|
46
|
+
preset_names[index] = preset.find('name').text.strip()
|
|
47
|
+
|
|
48
|
+
if preset.find('WritePrimIon').text.upper() == "TRUE":
|
|
49
|
+
val = preset.find('IndexPrimIon').text
|
|
50
|
+
preset_items[index][_key('PrimionIdx', '', 'INT')] = int(val)
|
|
51
|
+
|
|
52
|
+
if preset.find('WriteTransmission').text.upper() == "TRUE":
|
|
53
|
+
val = preset.find('IndexTransmission').text
|
|
54
|
+
preset_items[index][_key('TransmissionIdx', '', 'INT')] = int(val)
|
|
55
|
+
|
|
56
|
+
for item in preset.iterfind('item'):
|
|
57
|
+
if item.find('Write').text.upper() == "TRUE":
|
|
58
|
+
# device_index = item.find('DeviceIndex').text
|
|
59
|
+
ads_path = item.find('AdsPath').text
|
|
60
|
+
data_type = item.find('DataType').text
|
|
61
|
+
# page_name = item.find('PageName').text
|
|
62
|
+
name = item.find('Name').text
|
|
63
|
+
value_text = item.find('Value').text
|
|
64
|
+
|
|
65
|
+
key = _key(name, ads_path, data_type)
|
|
66
|
+
val = _parse_value[data_type](value_text)
|
|
67
|
+
preset_items[index][key] = val
|
|
68
|
+
|
|
69
|
+
return {index: (preset_names[index], preset_items[index]) for index in preset_names.keys()}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def setup_measurement_dir(config_dir=None, data_root_dir='D:/Data', suffix='',
|
|
73
|
+
date_fmt = "%Y_%m_%d__%H_%M_%S"):
|
|
74
|
+
"""Create a new directory for saving the measurement and set it up.
|
|
75
|
+
|
|
76
|
+
Optional: copy all files from the given config-directory.
|
|
77
|
+
|
|
78
|
+
data_root_dir: the base folder for storing new measurement-directories
|
|
79
|
+
suffix: will be appended to directory and data-file
|
|
80
|
+
date_fmt: format for the source-folder and -file to be timestamped
|
|
81
|
+
"""
|
|
82
|
+
import os
|
|
83
|
+
import glob
|
|
84
|
+
import shutil
|
|
85
|
+
from collections import namedtuple
|
|
86
|
+
from datetime import datetime
|
|
87
|
+
from itertools import chain
|
|
88
|
+
|
|
89
|
+
recipe = namedtuple('recipe', ['dirname', 'h5_file', 'pt_file', 'alarms_file'])
|
|
90
|
+
_pt_formats = ['*.ionipt']
|
|
91
|
+
_al_formats = ['*.alm']
|
|
92
|
+
# make directory with current timestamp:
|
|
93
|
+
now = datetime.now()
|
|
94
|
+
new_h5_file = os.path.abspath(os.path.join(
|
|
95
|
+
data_root_dir,
|
|
96
|
+
now.strftime(date_fmt) + suffix,
|
|
97
|
+
now.strftime(date_fmt) + suffix + '.h5',
|
|
98
|
+
))
|
|
99
|
+
new_recipe_dir = os.path.dirname(new_h5_file)
|
|
100
|
+
os.makedirs(new_recipe_dir, exist_ok=False) # may throw!
|
|
101
|
+
if not config_dir:
|
|
102
|
+
# we're done here..
|
|
103
|
+
return recipe(new_recipe_dir, new_h5_file, '', '')
|
|
104
|
+
|
|
105
|
+
# find the *first* matching file or an empty string if no match...
|
|
106
|
+
new_pt_file = next(chain.from_iterable(glob.iglob(config_dir + "/" + g) for g in _pt_formats), '')
|
|
107
|
+
new_al_file = next(chain.from_iterable(glob.iglob(config_dir + "/" + g) for g in _al_formats), '')
|
|
108
|
+
# ...and copy all files from the master-recipe-dir:
|
|
109
|
+
files2copy = glob.glob(config_dir + "/*")
|
|
110
|
+
for file in files2copy:
|
|
111
|
+
new_file = shutil.copy(file, new_recipe_dir)
|
|
112
|
+
try: # remove write permission (a.k.a. make files read-only)
|
|
113
|
+
mode = os.stat(file).st_mode
|
|
114
|
+
os.chmod(new_file, mode & ~stat.S_IWRITE)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
# well, we can't set write permission
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
return recipe(new_recipe_dir, new_h5_file, new_pt_file, new_al_file)
|
|
6
120
|
|
pytrms/peaktable.py
CHANGED
|
@@ -35,7 +35,7 @@ other custom formats.
|
|
|
35
35
|
>>> pt
|
|
36
36
|
<PeakTable (1) [21.0u]>
|
|
37
37
|
>>> pt[0]
|
|
38
|
-
<Peak
|
|
38
|
+
<Peak @ 21.0219+0.0000 [H3O+]>
|
|
39
39
|
|
|
40
40
|
Peaks may be modified and the PeakTable exported in the same format:
|
|
41
41
|
>>> pt[0].formula = 'H3O'
|
|
@@ -56,7 +56,9 @@ Peaks may be modified and the PeakTable exported in the same format:
|
|
|
56
56
|
"parent": "?",
|
|
57
57
|
"isotopic_abundance": 0.678,
|
|
58
58
|
"k_rate": 2.1,
|
|
59
|
-
"multiplier": 488
|
|
59
|
+
"multiplier": 488.0,
|
|
60
|
+
"resolution": 1000.0,
|
|
61
|
+
"shift": 0.0
|
|
60
62
|
}
|
|
61
63
|
]
|
|
62
64
|
}
|
|
@@ -144,14 +146,14 @@ class Peak:
|
|
|
144
146
|
return self.center == round(float(other), Peak._exact_decimals)
|
|
145
147
|
|
|
146
148
|
def __hash__(self):
|
|
147
|
-
return hash(str(self.center)
|
|
149
|
+
return hash(str(self.center) + self.label)
|
|
148
150
|
|
|
149
151
|
def __float__(self):
|
|
150
152
|
return self.center
|
|
151
153
|
|
|
152
154
|
def __repr__(self):
|
|
153
|
-
return '<%s
|
|
154
|
-
self.
|
|
155
|
+
return '<%s @ %.4f+%.4f [%s]>' % (self.__class__.__name__,
|
|
156
|
+
self.center, self.shift, self.label)
|
|
155
157
|
|
|
156
158
|
|
|
157
159
|
class PeakTable:
|
pytrms/readers/ionitof_reader.py
CHANGED
|
@@ -7,19 +7,10 @@ import numpy as np
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
9
|
from .._base import itype
|
|
10
|
+
from ..helpers import convert_labview_to_posix
|
|
10
11
|
|
|
11
12
|
__all__ = ['IoniTOFReader', 'GroupNotFoundError']
|
|
12
13
|
|
|
13
|
-
def convert_labview_to_posix(lv_time_utc, utc_offset_sec):
|
|
14
|
-
'''Create a `pandas.Timestamp` from LabView time.'''
|
|
15
|
-
# change epoch from 01.01.1904 to 01.01.1970:
|
|
16
|
-
posix_time = lv_time_utc - 2082844800
|
|
17
|
-
# the tz must be specified in isoformat like '+02:30'..
|
|
18
|
-
tz_sec = int(utc_offset_sec)
|
|
19
|
-
tz_designator = '{0}{1:02d}:{2:02d}'.format(
|
|
20
|
-
'+' if tz_sec >= 0 else '-', tz_sec // 3600, tz_sec % 3600 // 60)
|
|
21
|
-
|
|
22
|
-
return pd.Timestamp(posix_time, unit='s', tz=tz_designator)
|
|
23
14
|
|
|
24
15
|
class GroupNotFoundError(KeyError):
|
|
25
16
|
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytrms
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Python bundle for proton-transfer reaction mass-spectrometry (PTR-MS).
|
|
5
5
|
License: GPL-2.0
|
|
6
6
|
Author: Moritz Koenemann
|
|
@@ -15,5 +15,5 @@ Requires-Dist: h5py (>=3.12.1,<4.0.0)
|
|
|
15
15
|
Requires-Dist: matplotlib (>=3.9.2,<4.0.0)
|
|
16
16
|
Requires-Dist: paho-mqtt (>=1.6.1,<1.7.0)
|
|
17
17
|
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
18
|
-
Requires-Dist: pyModbusTCP (>=0.
|
|
18
|
+
Requires-Dist: pyModbusTCP (>=0.1.9)
|
|
19
19
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
pytrms/__init__.py,sha256=bEgQmBOD5ftYjdwWCsZALJzhodK8CsOxI24V8Dm9Vcg,925
|
|
2
|
+
pytrms/_base/__init__.py,sha256=GBALqAy1kUPMc0CWnWRmn_Cg_HGKGCeE-V2rdZEmN8A,836
|
|
3
|
+
pytrms/_base/ioniclient.py,sha256=f4xWW3PL0p1gP7pczBaDEva3WUUb_J73n7yTIY5pW4s,826
|
|
4
|
+
pytrms/_base/mqttclient.py,sha256=iua-AK7S_3rH1hsuLepqIjf7ivF5Ihb6T9OJNafJXXE,4233
|
|
5
|
+
pytrms/_version.py,sha256=fXD69jGmDocNcc-uipbNIhjWxC7EuyhPUQ6BrBvK0Wg,854
|
|
6
|
+
pytrms/clients/__init__.py,sha256=RejfXqq7zSKLMZbUHdxtBvLiyeshLX-JQzgthmpYeMw,1404
|
|
7
|
+
pytrms/clients/db_api.py,sha256=wEEhPUsdbqSH8TsYRICeYxAfBzdyNekM-rYe4xlCXkM,7248
|
|
8
|
+
pytrms/clients/ioniclient.py,sha256=pHGzaNtKuVrjZ2O2nTmZYJObNMDkxNwLcNerlFit1vA,2477
|
|
9
|
+
pytrms/clients/modbus.py,sha256=BbluHm8iYQY4V2Wbxd4eSiWJtIORyxRWRjhJo1_1JzQ,20585
|
|
10
|
+
pytrms/clients/mqtt.py,sha256=H8uRBGcB4j4MaIlN7Hk_DcRnINJ7hxSxSA6sw4YJKAM,31041
|
|
11
|
+
pytrms/clients/ssevent.py,sha256=umNeWMEYS65qLjmD9zawHTqzatnMDphFDALaxOgCKao,2731
|
|
12
|
+
pytrms/compose/__init__.py,sha256=gRkwGezTsuiMLA4jr5hhQsY7XX_FonBeWcvDfDuMFnY,30
|
|
13
|
+
pytrms/compose/composition.py,sha256=hlV8g6n6HaLLLKileSh7sk8EtwPvaIQjOFXCEKLDKJ0,12161
|
|
14
|
+
pytrms/data/IoniTofPrefs.ini,sha256=e1nU7Hyuh0efpgfN-G5-ERAFC6ZeehUiotsM4VtZIT0,1999
|
|
15
|
+
pytrms/data/ParaIDs.csv,sha256=eWQxmHFfeTSxFfMcFpqloiZWDyK4VLZaV9zCnzLHNYs,27996
|
|
16
|
+
pytrms/helpers.py,sha256=QfJgdQ6b0RcyiAuNHbDGGfuRfeZIs20dPF5flZVR12o,4979
|
|
17
|
+
pytrms/instrument.py,sha256=OIFTbS6fuhos6oYMsrA_qdgvs_7T-H-sOSl1a0qpxZ8,3977
|
|
18
|
+
pytrms/measurement.py,sha256=RqACqedT0JQDkv5cmKxcXVSgl1n0Fbp1mQj81aOsZkg,5507
|
|
19
|
+
pytrms/peaktable.py,sha256=Ms0_dyHg8e6Oc8mwSTk8rD70EPjosX92pDXstf4f6Tk,17572
|
|
20
|
+
pytrms/plotting/__init__.py,sha256=vp5mAa3npo4kP5wvXXNDxKnryFK693P4PSwC-BxEUH8,66
|
|
21
|
+
pytrms/plotting/plotting.py,sha256=t7RXUhjOwXchtYXVgb0rJPD_9aVMDCT6DCAGu8TZQfE,702
|
|
22
|
+
pytrms/readers/__init__.py,sha256=F1ZX4Jv7NcY8nuSWnIbwjYnNFC2JsTnEp0BnfBHUMSM,76
|
|
23
|
+
pytrms/readers/ionitof_reader.py,sha256=zWKIdkeueRMDMxknUGOu68kUrPJz0rjVu4D_aoZUQQ4,18128
|
|
24
|
+
pytrms-0.9.1.dist-info/LICENSE,sha256=GJsa-V1mEVHgVM6hDJGz11Tk3k0_7PsHTB-ylHb3Fns,18431
|
|
25
|
+
pytrms-0.9.1.dist-info/METADATA,sha256=hmNvBCwFj49gPdStWhw5DrWneupksW5DQuPsX64on7Q,763
|
|
26
|
+
pytrms-0.9.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
27
|
+
pytrms-0.9.1.dist-info/RECORD,,
|
pytrms/clients/dirigent.py
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
from contextlib import contextmanager
|
|
4
|
-
import logging
|
|
5
|
-
|
|
6
|
-
import requests
|
|
7
|
-
|
|
8
|
-
from . import ionitof_url
|
|
9
|
-
|
|
10
|
-
log = logging.getLogger()
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Template:
|
|
14
|
-
'''A template for uploading a collection item.
|
|
15
|
-
|
|
16
|
-
>>> t = Template()
|
|
17
|
-
>>> t.template == Template.default_template
|
|
18
|
-
True
|
|
19
|
-
|
|
20
|
-
>>> t.render('AME_FooNumber', 42)
|
|
21
|
-
'{"template": {"data": [{"name": "ParaID", "value": "AME_FooNumber", "prompt": "the
|
|
22
|
-
parameter ID"}, {"name": "ValAsString", "value": "42", "prompt": "the new value"}]}}'
|
|
23
|
-
|
|
24
|
-
'''
|
|
25
|
-
|
|
26
|
-
default_template = {
|
|
27
|
-
"data": [
|
|
28
|
-
{"name": "ParaID", "value": "AME_RunNumber", "prompt": "the parameter ID"},
|
|
29
|
-
{"name": "ValAsString", "value": "5.000000", "prompt": "the new value"},
|
|
30
|
-
{"name": "DataType", "value": "", "prompt": "datatype (int, float, string)"},
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@staticmethod
|
|
35
|
-
def download(url=ionitof_url, endpoint='/api/schedule'):
|
|
36
|
-
r = requests.get(url + endpoint, headers={'accept': 'application/vnd.collection+json'})
|
|
37
|
-
r.raise_for_status()
|
|
38
|
-
j = r.json()
|
|
39
|
-
|
|
40
|
-
return Template(j["collection"]["template"])
|
|
41
|
-
|
|
42
|
-
def __init__(self, template=None):
|
|
43
|
-
if template is None:
|
|
44
|
-
template = Template.default_template
|
|
45
|
-
self.template = dict(template)
|
|
46
|
-
|
|
47
|
-
self._inserts = dict() # cache data-items sorted by semantic meaning..
|
|
48
|
-
for insert in self.template["data"]:
|
|
49
|
-
if 'ID' in insert["name"] or 'parameter' in insert["prompt"]:
|
|
50
|
-
self._inserts["parID"] = insert
|
|
51
|
-
if 'set' in insert["name"].lower() or 'value' in insert["prompt"]:
|
|
52
|
-
self._inserts["value"] = insert
|
|
53
|
-
if 'typ' in insert["name"].lower() or 'datatype' in insert["prompt"]:
|
|
54
|
-
self._inserts["dtype"] = insert
|
|
55
|
-
|
|
56
|
-
assert len(self._inserts) == 3, "missing or unknown name in template"
|
|
57
|
-
|
|
58
|
-
def render(self, parID, value):
|
|
59
|
-
"""Prepare a request for uploading."""
|
|
60
|
-
dtype = 'float'
|
|
61
|
-
if isinstance(value, int): dtype = 'int'
|
|
62
|
-
if isinstance(value, str): dtype = 'string'
|
|
63
|
-
|
|
64
|
-
parID_insert = dict(self._inserts["parID"])
|
|
65
|
-
value_insert = dict(self._inserts["value"])
|
|
66
|
-
dtype_insert = dict(self._inserts["dtype"])
|
|
67
|
-
|
|
68
|
-
parID_insert.update(value=str(parID)),
|
|
69
|
-
value_insert.update(value=str(value)),
|
|
70
|
-
dtype_insert.update(value=str(dtype)),
|
|
71
|
-
|
|
72
|
-
return json.dumps({
|
|
73
|
-
"template": dict(data=[
|
|
74
|
-
parID_insert,
|
|
75
|
-
value_insert,
|
|
76
|
-
dtype_insert,
|
|
77
|
-
])}
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
def render_many(self, new_values):
|
|
81
|
-
for key, value in new_values.items():
|
|
82
|
-
yield self.render(key, value)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class Dirigent:
|
|
86
|
-
|
|
87
|
-
def __init__(self, url=ionitof_url, template=None):
|
|
88
|
-
if template is None:
|
|
89
|
-
template = Template.download(url)
|
|
90
|
-
|
|
91
|
-
self.url = url
|
|
92
|
-
self.template = template
|
|
93
|
-
self._session = None # TODO :: ?
|
|
94
|
-
|
|
95
|
-
def push(self, parID, new_value, future_cycle):
|
|
96
|
-
uri = self.url + '/api/schedule/' + str(int(future_cycle))
|
|
97
|
-
payload = self.template.render(parID, new_value)
|
|
98
|
-
r = self._make_request('PUT', uri, payload=payload)
|
|
99
|
-
|
|
100
|
-
return r.status_code
|
|
101
|
-
|
|
102
|
-
def push_filename(self, path, future_cycle):
|
|
103
|
-
return self.push('ACQ_SRV_SetFullStorageFile', path.replace('/', '\\'), future_cycle - 2)
|
|
104
|
-
|
|
105
|
-
def find_scheduled(self, parID):
|
|
106
|
-
uri = self.url + '/api/schedule/search'
|
|
107
|
-
r = self._make_request('GET', uri, params={'name': str(parID)})
|
|
108
|
-
j = r.json()
|
|
109
|
-
|
|
110
|
-
return [item['href'].split('/')[-1] for item in j['collection']['items']]
|
|
111
|
-
|
|
112
|
-
def _make_request(self, method, uri, params=None, payload=None):
|
|
113
|
-
if self._session is None:
|
|
114
|
-
session = requests # not using a session..
|
|
115
|
-
else:
|
|
116
|
-
session = self._session
|
|
117
|
-
|
|
118
|
-
try:
|
|
119
|
-
r = session.request(method, uri, params=params, data=payload, headers={
|
|
120
|
-
'content-type': 'application/vnd.collection+json'
|
|
121
|
-
})
|
|
122
|
-
except requests.exceptions.ConnectionError as exc:
|
|
123
|
-
# Note: the LabVIEW-webservice seems to implement a weird HTTP:
|
|
124
|
-
# we may get a [85] Custom Error (Bad status line) from urllib3
|
|
125
|
-
# even though we just mis-spelled a parameter-ID ?!
|
|
126
|
-
log.error(exc)
|
|
127
|
-
raise KeyError(str(params, payload))
|
|
128
|
-
|
|
129
|
-
log.debug(f"request to <{uri}> returned [{r.status_code}]")
|
|
130
|
-
if not r.ok and payload:
|
|
131
|
-
log.error(payload)
|
|
132
|
-
r.raise_for_status()
|
|
133
|
-
|
|
134
|
-
return r
|
|
135
|
-
|
|
136
|
-
def wait_until(self, future_cycle):
|
|
137
|
-
if self._session is None:
|
|
138
|
-
session = requests # not using a session..
|
|
139
|
-
else:
|
|
140
|
-
session = self._session
|
|
141
|
-
|
|
142
|
-
r = session.get(self.url + '/api/timing/' + str(int(future_cycle)))
|
|
143
|
-
# ...
|
|
144
|
-
if r.status_code == 410:
|
|
145
|
-
# 410 Client Error: Gone
|
|
146
|
-
log.warning("we're late, better return immediately!")
|
|
147
|
-
return r.status_code
|
|
148
|
-
|
|
149
|
-
r.raise_for_status()
|
|
150
|
-
log.debug(f"waited until {r.json()['TimeCycle']}")
|
|
151
|
-
|
|
152
|
-
return r.status_code
|
|
153
|
-
|
|
154
|
-
@contextmanager
|
|
155
|
-
def open_session(self):
|
|
156
|
-
'''open a session for faster upload (to be used as a contextmanager).'''
|
|
157
|
-
if self._session is None:
|
|
158
|
-
self._session = requests.Session()
|
|
159
|
-
try:
|
|
160
|
-
yield self
|
|
161
|
-
finally:
|
|
162
|
-
self._session.close()
|
|
163
|
-
self._session = None
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if __name__ == '__main__':
|
|
167
|
-
import doctest
|
|
168
|
-
doctest.testmod(verbose=True, optionflags=doctest.ELLIPSIS)
|
|
169
|
-
|
pytrms/tracebuffer.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import json
|
|
3
|
-
import queue
|
|
4
|
-
from itertools import chain
|
|
5
|
-
from enum import Enum
|
|
6
|
-
from threading import Thread, Condition
|
|
7
|
-
|
|
8
|
-
import pandas as pd
|
|
9
|
-
|
|
10
|
-
from .helpers import convert_labview_to_posix, PTRConnectionError
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def parse(response, trace='raw'):
|
|
14
|
-
jsonized = json.loads(response)
|
|
15
|
-
info = jsonized['TimeCycle']
|
|
16
|
-
ts = convert_labview_to_posix(info['AbsTime'])
|
|
17
|
-
|
|
18
|
-
data = [list(info.values())] + [a['Data'] for a in jsonized['AddData']] + [jsonized[trace]]
|
|
19
|
-
desc = [list(info.keys())] + [a['Desc'] for a in jsonized['AddData']] + [jsonized['masses']]
|
|
20
|
-
chained_data = chain(*data)
|
|
21
|
-
chained_desc = chain(*desc)
|
|
22
|
-
|
|
23
|
-
return pd.Series(data=chained_data, index=chained_desc, name=ts)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class TraceBuffer(Thread):
|
|
27
|
-
|
|
28
|
-
poll = 0.2 # seconds
|
|
29
|
-
|
|
30
|
-
class State(Enum):
|
|
31
|
-
CONNECTING = -1
|
|
32
|
-
IDLE = 0
|
|
33
|
-
ACTIVE = 1
|
|
34
|
-
|
|
35
|
-
def __init__(self, client):
|
|
36
|
-
"""'client' must provide a `.get_traces()` method that returns raw json data.
|
|
37
|
-
"""
|
|
38
|
-
Thread.__init__(self)
|
|
39
|
-
self.daemon = True
|
|
40
|
-
self.client = client
|
|
41
|
-
self.queue = queue.Queue()
|
|
42
|
-
self.state = TraceBuffer.State.CONNECTING
|
|
43
|
-
self._cond = Condition()
|
|
44
|
-
|
|
45
|
-
def is_connected(self):
|
|
46
|
-
return self.state != TraceBuffer.State.CONNECTING
|
|
47
|
-
|
|
48
|
-
def is_idle(self):
|
|
49
|
-
return self.state == TraceBuffer.State.IDLE
|
|
50
|
-
|
|
51
|
-
def wait_for_connection(self, timeout=5):
|
|
52
|
-
'''
|
|
53
|
-
will raise a PTRConnectionError if not connected after `timeout`
|
|
54
|
-
'''
|
|
55
|
-
waited = 0
|
|
56
|
-
dt = 0.01
|
|
57
|
-
while not self.is_connected():
|
|
58
|
-
waited += dt
|
|
59
|
-
if waited > timeout:
|
|
60
|
-
raise PTRConnectionError('no connection to instrument')
|
|
61
|
-
time.sleep(dt)
|
|
62
|
-
|
|
63
|
-
def run(self):
|
|
64
|
-
last = -753 # the year Rome was founded is never a valid cycle
|
|
65
|
-
while True:
|
|
66
|
-
#TODO :: das kann ja gar nicht funktionieren!?!?
|
|
67
|
-
#if not self.is_connected():
|
|
68
|
-
# time.sleep(self.poll)
|
|
69
|
-
# continue
|
|
70
|
-
|
|
71
|
-
time.sleep(self.poll)
|
|
72
|
-
|
|
73
|
-
with self._cond: # .acquire()`s the underlying lock
|
|
74
|
-
raw = self.client.get_traces()
|
|
75
|
-
#try:
|
|
76
|
-
#except PTRConnectionError as exc:
|
|
77
|
-
# print(exc)
|
|
78
|
-
# break
|
|
79
|
-
if not len(raw):
|
|
80
|
-
continue
|
|
81
|
-
|
|
82
|
-
jsonized = json.loads(raw)
|
|
83
|
-
ts = jsonized['TimeCycle']['AbsTime']
|
|
84
|
-
oc = jsonized['TimeCycle']['OverallCycle']
|
|
85
|
-
# the client returns the "current", i.e. last known trace data, even if
|
|
86
|
-
# the machine is currently stopped. we want to definitely reflect this
|
|
87
|
-
# idle state of the (actual) machine in our Python objects!
|
|
88
|
-
# TODO :: *ideally*, the state is returned by a webAPI-call.. but as long
|
|
89
|
-
# as this doesn't work perfectly, let's just do the next best thing and
|
|
90
|
-
# watch the current cycle:
|
|
91
|
-
if last < 0: last = oc
|
|
92
|
-
|
|
93
|
-
if oc > last:
|
|
94
|
-
pd_series = parse(raw)
|
|
95
|
-
self.queue.put(pd_series)
|
|
96
|
-
self.state = TraceBuffer.State.ACTIVE
|
|
97
|
-
else:
|
|
98
|
-
self.state = TraceBuffer.State.IDLE
|
|
99
|
-
last = oc
|
|
100
|
-
|
|
101
|
-
# This method releases the underlying lock, and then blocks until it is
|
|
102
|
-
# awakened by a notify() or notify_all() call for the same condition variable
|
|
103
|
-
# in another thread, or until the optional timeout occurs. Once awakened or
|
|
104
|
-
# timed out, it re-acquires the lock and returns. The return value is True
|
|
105
|
-
# unless a given timeout expired, in which case it is False.
|
|
106
|
-
if self._cond.wait(self.poll):
|
|
107
|
-
break
|
|
108
|
-
|
pytrms-0.9.0.dist-info/RECORD
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
pytrms/__init__.py,sha256=U4ixhTjfFkiVlLXV4JXcRq-eLFkdKlGlU4e52Zlbpsg,970
|
|
2
|
-
pytrms/_base/__init__.py,sha256=GBALqAy1kUPMc0CWnWRmn_Cg_HGKGCeE-V2rdZEmN8A,836
|
|
3
|
-
pytrms/_base/ioniclient.py,sha256=f4xWW3PL0p1gP7pczBaDEva3WUUb_J73n7yTIY5pW4s,826
|
|
4
|
-
pytrms/_base/mqttclient.py,sha256=iua-AK7S_3rH1hsuLepqIjf7ivF5Ihb6T9OJNafJXXE,4233
|
|
5
|
-
pytrms/_version.py,sha256=fXD69jGmDocNcc-uipbNIhjWxC7EuyhPUQ6BrBvK0Wg,854
|
|
6
|
-
pytrms/clients/__init__.py,sha256=RejfXqq7zSKLMZbUHdxtBvLiyeshLX-JQzgthmpYeMw,1404
|
|
7
|
-
pytrms/clients/db_api.py,sha256=exgE4iUHdACprztYYs330NzYI88f6TwF8PjtBzohgjk,6658
|
|
8
|
-
pytrms/clients/dirigent.py,sha256=9WZPfqXxLmFCcT2RC8wxm7uixe4n-Y8vbsdpzc61tsI,5742
|
|
9
|
-
pytrms/clients/ioniclient.py,sha256=5s3GPDr6AKH4Ia0Jix6UI8Xie_t3muKxr218MNRk2Yw,2853
|
|
10
|
-
pytrms/clients/modbus.py,sha256=E1IfoHJm2gVHo-GcKDWba359ii5e0Ymaplv-DO88FqM,19223
|
|
11
|
-
pytrms/clients/mqtt.py,sha256=QRJgC10GWFKzT3r4yNGOafXshgHlA5poGEzouEporEU,30231
|
|
12
|
-
pytrms/clients/ssevent.py,sha256=OziYX9amJunVzTN0MiQmLg2mICHdnVC0LXT5o_ZcwcE,2546
|
|
13
|
-
pytrms/compose/__init__.py,sha256=gRkwGezTsuiMLA4jr5hhQsY7XX_FonBeWcvDfDuMFnY,30
|
|
14
|
-
pytrms/compose/composition.py,sha256=c3ae3PgQg0b741RhRU6oQMAiM7B2xRvbweGp0tCZatc,7906
|
|
15
|
-
pytrms/data/IoniTofPrefs.ini,sha256=e1nU7Hyuh0efpgfN-G5-ERAFC6ZeehUiotsM4VtZIT0,1999
|
|
16
|
-
pytrms/data/ParaIDs.csv,sha256=eWQxmHFfeTSxFfMcFpqloiZWDyK4VLZaV9zCnzLHNYs,27996
|
|
17
|
-
pytrms/helpers.py,sha256=pNLYWlHM1n7vOkCi2vwESMG5baIJ5v2OD4z94XpXU1k,108
|
|
18
|
-
pytrms/instrument.py,sha256=OIFTbS6fuhos6oYMsrA_qdgvs_7T-H-sOSl1a0qpxZ8,3977
|
|
19
|
-
pytrms/measurement.py,sha256=RqACqedT0JQDkv5cmKxcXVSgl1n0Fbp1mQj81aOsZkg,5507
|
|
20
|
-
pytrms/peaktable.py,sha256=b3KkILn5DctEfTi1-tOC9LZv1yrRcoH23VaE5JEqyk4,17536
|
|
21
|
-
pytrms/plotting/__init__.py,sha256=vp5mAa3npo4kP5wvXXNDxKnryFK693P4PSwC-BxEUH8,66
|
|
22
|
-
pytrms/plotting/plotting.py,sha256=t7RXUhjOwXchtYXVgb0rJPD_9aVMDCT6DCAGu8TZQfE,702
|
|
23
|
-
pytrms/readers/__init__.py,sha256=F1ZX4Jv7NcY8nuSWnIbwjYnNFC2JsTnEp0BnfBHUMSM,76
|
|
24
|
-
pytrms/readers/ionitof_reader.py,sha256=vhwVfhJ-ly8I_02okpgxoVqnB0QRDuqkufGZY21vFi4,18583
|
|
25
|
-
pytrms/tracebuffer.py,sha256=30HfMJtxZ4Dnf4mPeyinXqESOA-0Xpu9FR4jaBvb_nA,3912
|
|
26
|
-
pytrms-0.9.0.dist-info/LICENSE,sha256=GJsa-V1mEVHgVM6hDJGz11Tk3k0_7PsHTB-ylHb3Fns,18431
|
|
27
|
-
pytrms-0.9.0.dist-info/METADATA,sha256=sTHS7ZvxCdgcaTj_hCI_Vb8erA1nIUBCs4JMGb06mRg,770
|
|
28
|
-
pytrms-0.9.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
29
|
-
pytrms-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|