rtint 0.0.3__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.
- rtint/_version.py +24 -0
- rtint/rtint_acquisition.py +327 -0
- rtint/rtint_counter.py +202 -0
- rtint/rtint_ethercat.py +127 -0
- rtint/rtint_filter.py +872 -0
- rtint/rtint_generator.py +577 -0
- rtint/rtint_hardware.py +336 -0
- rtint/rtint_lut.py +143 -0
- rtint/rtint_motor.py +191 -0
- rtint/rtint_parameter.py +105 -0
- rtint/rtint_regul.py +310 -0
- rtint/rtint_signal.py +42 -0
- rtint/rtint_trigger.py +107 -0
- rtint/rtint_utils.py +648 -0
- rtint-0.0.3.dist-info/METADATA +74 -0
- rtint-0.0.3.dist-info/RECORD +19 -0
- rtint-0.0.3.dist-info/WHEEL +5 -0
- rtint-0.0.3.dist-info/licenses/LICENSE +238 -0
- rtint-0.0.3.dist-info/top_level.txt +1 -0
rtint/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.0.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 3)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of the mechatronic project
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) Beamline Control Unit, ESRF
|
|
6
|
+
# Distributed under the GNU LGPLv3. See LICENSE for more info.
|
|
7
|
+
|
|
8
|
+
import gevent
|
|
9
|
+
import random
|
|
10
|
+
import string
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from resyst.client.acq import Acq, AcqState
|
|
14
|
+
from resyst.common.acq_conf import AcqConf
|
|
15
|
+
from tabulate import tabulate
|
|
16
|
+
|
|
17
|
+
from rtint.rtint_utils import status_message
|
|
18
|
+
|
|
19
|
+
RED = "\033[31m"
|
|
20
|
+
GREEN = "\033[32m"
|
|
21
|
+
RESET = "\033[0m"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RtintHdwAcquisition:
|
|
25
|
+
def __init__(self, tg):
|
|
26
|
+
self._tg = tg
|
|
27
|
+
self._system = tg._system
|
|
28
|
+
self._program = tg._program
|
|
29
|
+
|
|
30
|
+
self._acq: Acq | None = None
|
|
31
|
+
|
|
32
|
+
def __info__(self, debug=False):
|
|
33
|
+
if len(self._program.acqs) == 0:
|
|
34
|
+
return "\n No Loaded acquisition"
|
|
35
|
+
|
|
36
|
+
lines = [["Name", "State", "Decimation", "Nbp", "Signals"]]
|
|
37
|
+
for acq in self._program.acqs:
|
|
38
|
+
lines.append(
|
|
39
|
+
[
|
|
40
|
+
acq.conf.name,
|
|
41
|
+
acq.status.state.name,
|
|
42
|
+
acq.conf.decimation,
|
|
43
|
+
f"{acq.conf.nbp:d}",
|
|
44
|
+
acq.conf.signal_paths[0][len(self._program.name) + 1 :],
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
for i in range(1, len(acq.conf.signal_paths)):
|
|
48
|
+
lines.append(
|
|
49
|
+
[
|
|
50
|
+
"",
|
|
51
|
+
"",
|
|
52
|
+
"",
|
|
53
|
+
"",
|
|
54
|
+
acq.conf.signal_paths[i][len(self._program.name) + 1 :],
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
return "\n" + tabulate(lines, headers="firstrow", tablefmt="grid", stralign="left")
|
|
58
|
+
|
|
59
|
+
def prepare(
|
|
60
|
+
self,
|
|
61
|
+
nsample,
|
|
62
|
+
counter_list,
|
|
63
|
+
decimation=1,
|
|
64
|
+
name=None,
|
|
65
|
+
filter_path="",
|
|
66
|
+
start_path="",
|
|
67
|
+
start_pre_samples=0,
|
|
68
|
+
):
|
|
69
|
+
return self._create_acq(
|
|
70
|
+
counter_list,
|
|
71
|
+
nsample,
|
|
72
|
+
decimation=decimation,
|
|
73
|
+
name=name,
|
|
74
|
+
filter_path=filter_path,
|
|
75
|
+
start_path=start_path,
|
|
76
|
+
start_pre_samples=start_pre_samples,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def prepare_time(
|
|
80
|
+
self,
|
|
81
|
+
time,
|
|
82
|
+
counter_list,
|
|
83
|
+
decimation=1,
|
|
84
|
+
name=None,
|
|
85
|
+
filter_path="",
|
|
86
|
+
start_path="",
|
|
87
|
+
start_pre_samples=0,
|
|
88
|
+
):
|
|
89
|
+
nbp = int(time / self._tg._Ts / decimation)
|
|
90
|
+
return self._create_acq(
|
|
91
|
+
counter_list,
|
|
92
|
+
nbp,
|
|
93
|
+
decimation=decimation,
|
|
94
|
+
name=name,
|
|
95
|
+
filter_path=filter_path,
|
|
96
|
+
start_path=start_path,
|
|
97
|
+
start_pre_samples=start_pre_samples,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def start(self, wait=False, silent=True, name=None):
|
|
101
|
+
"""Use to start the acquisition (the real-time target will start sending data)"""
|
|
102
|
+
self._program.start_acqs([self._get_acq_from_name(name)])
|
|
103
|
+
|
|
104
|
+
if wait:
|
|
105
|
+
self._wait_finished(silent=silent)
|
|
106
|
+
|
|
107
|
+
def stop(self, name=None):
|
|
108
|
+
acq = self._get_acq_from_name(name)
|
|
109
|
+
if acq is not None:
|
|
110
|
+
acq.stop()
|
|
111
|
+
|
|
112
|
+
def get_data(self, name=None, display=False, debug=False):
|
|
113
|
+
"""
|
|
114
|
+
Used to get all data of an acquistion.
|
|
115
|
+
It will wait for the acquisition to be finished (i.e. all data are received by the Resyst server).
|
|
116
|
+
The acquisition is then removed from the server.
|
|
117
|
+
"""
|
|
118
|
+
acq = self._get_acq_from_name(name)
|
|
119
|
+
result = {
|
|
120
|
+
self._tg.counter._get_counter_from_full_path(signal_path).name: []
|
|
121
|
+
for signal_path in acq.conf.signal_paths
|
|
122
|
+
}
|
|
123
|
+
timestamp = []
|
|
124
|
+
|
|
125
|
+
with status_message(enable=display) as update:
|
|
126
|
+
while (self._is_running(name)) or (acq.nb_sample_to_read > 0):
|
|
127
|
+
if acq.nb_sample_to_read > 0:
|
|
128
|
+
n_to_read = acq.nb_sample_to_read
|
|
129
|
+
data_acq = acq.get_data(n_to_read)
|
|
130
|
+
timestamp.append(data_acq["timestamp"])
|
|
131
|
+
|
|
132
|
+
for signal_path in acq.conf.signal_paths:
|
|
133
|
+
counter_name = (
|
|
134
|
+
self._tg.counter._get_counter_from_full_path(
|
|
135
|
+
signal_path
|
|
136
|
+
).name
|
|
137
|
+
)
|
|
138
|
+
val = data_acq[signal_path]
|
|
139
|
+
result[counter_name].append(val)
|
|
140
|
+
|
|
141
|
+
update(
|
|
142
|
+
f"Waiting Acquisition to terminate ({np.sum([t.size for t in timestamp])}/{acq.conf.nbp})"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
gevent.sleep(0.2)
|
|
146
|
+
|
|
147
|
+
# concatenate the bunches for each signal (so result looks like the old get_data)
|
|
148
|
+
for k in list(result.keys()):
|
|
149
|
+
result[k] = np.concatenate(result[k], axis=0)
|
|
150
|
+
|
|
151
|
+
if debug:
|
|
152
|
+
if self._is_finished(name):
|
|
153
|
+
print(
|
|
154
|
+
"Acquisition correctly acquired all the points."
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
print(
|
|
158
|
+
"Acquisition was not finished (not all points could be acquired by the Resyst server)."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Check if no point is missing by looking at the Timestamp
|
|
162
|
+
timestamp = np.concatenate(timestamp, axis=0)
|
|
163
|
+
timestamp_nom_value = 1e6 * (
|
|
164
|
+
self._tg._Ts * acq.conf.decimation
|
|
165
|
+
) # Expected timestamp increase in [us]
|
|
166
|
+
step_diff = np.rint(
|
|
167
|
+
np.diff(timestamp) / timestamp_nom_value
|
|
168
|
+
) # Should always be equal to one
|
|
169
|
+
|
|
170
|
+
# Check if there are some missing points
|
|
171
|
+
if not np.all(step_diff == 1):
|
|
172
|
+
print(
|
|
173
|
+
f"WARNING: There are {np.sum(step_diff - 1)} missing points (maximum {np.max(step_diff) - 1} consecutive missing points)"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Check if correct number of points
|
|
177
|
+
if (
|
|
178
|
+
len(
|
|
179
|
+
result[
|
|
180
|
+
self._tg.counter._get_counter_from_full_path(
|
|
181
|
+
acq.conf.signal_paths[0]
|
|
182
|
+
).name
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
!= acq.conf.nbp
|
|
186
|
+
):
|
|
187
|
+
print(
|
|
188
|
+
f"WARNING: Incorect number of points: ({len(result[self._tg.counter._get_counter_from_full_path(acq.conf.signal_paths[0]).name])}/{acq.conf.nbp})"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Delete the acquisition (after all data have been retrieved)
|
|
192
|
+
self._program.remove_acq(acq.conf.name)
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
def get_available_data(self, name=None, max_nbp=None):
|
|
197
|
+
acq = self._get_acq_from_name(name)
|
|
198
|
+
result = {}
|
|
199
|
+
|
|
200
|
+
# If max_nbp is specified, get at most this number of points
|
|
201
|
+
# Otherwise, get all the available points
|
|
202
|
+
if max_nbp is not None:
|
|
203
|
+
n_to_read = min(max_nbp, acq.nb_sample_to_read)
|
|
204
|
+
else:
|
|
205
|
+
n_to_read = acq.nb_sample_to_read
|
|
206
|
+
|
|
207
|
+
data_acq = acq.get_data(n_to_read)
|
|
208
|
+
|
|
209
|
+
for signal_path in acq.conf.signal_paths:
|
|
210
|
+
counter_name = self._tg.counter._get_counter_from_full_path(
|
|
211
|
+
signal_path
|
|
212
|
+
).name
|
|
213
|
+
val = data_acq[signal_path]
|
|
214
|
+
result[counter_name] = val
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def _create_acq(
|
|
219
|
+
self,
|
|
220
|
+
counters,
|
|
221
|
+
nbp,
|
|
222
|
+
decimation=1,
|
|
223
|
+
name=None,
|
|
224
|
+
filter_path="",
|
|
225
|
+
start_path="",
|
|
226
|
+
start_pre_samples=0,
|
|
227
|
+
):
|
|
228
|
+
"""
|
|
229
|
+
Register one Acquisition on the Resyst server
|
|
230
|
+
"""
|
|
231
|
+
if name is None:
|
|
232
|
+
# Create random name if not specified
|
|
233
|
+
name = "".join(random.choices(string.ascii_uppercase, k=5))
|
|
234
|
+
|
|
235
|
+
if filter_path != "" and start_path != "":
|
|
236
|
+
print(
|
|
237
|
+
"WARNING: filter_path and start_path cannot be used at the same time"
|
|
238
|
+
)
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
if filter_path != "": # Trigerred acquisition
|
|
242
|
+
name = "trig_" + name
|
|
243
|
+
if decimation > 1:
|
|
244
|
+
print(
|
|
245
|
+
"WARNING: When filter_path is used, decimation should be equal to 1"
|
|
246
|
+
)
|
|
247
|
+
elif start_path != "": # Start Condition : Monitoring
|
|
248
|
+
name = "moni_" + name
|
|
249
|
+
else: # Normal Acquisition
|
|
250
|
+
name = "acq_" + name
|
|
251
|
+
|
|
252
|
+
# Force no pre-samples when not using a start trigger
|
|
253
|
+
if start_path == "":
|
|
254
|
+
start_pre_samples = 0
|
|
255
|
+
|
|
256
|
+
acq_conf = AcqConf(
|
|
257
|
+
name=name,
|
|
258
|
+
signal_paths=[counter._full_path for counter in counters],
|
|
259
|
+
nbp=nbp,
|
|
260
|
+
decimation=decimation,
|
|
261
|
+
filter_path=filter_path,
|
|
262
|
+
start_path=start_path,
|
|
263
|
+
start_pre_samples=start_pre_samples,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Automatically add the acquisition
|
|
267
|
+
self._acq = self._program.add_acq(acq_conf)
|
|
268
|
+
return name
|
|
269
|
+
|
|
270
|
+
def _remove_acqs(self, acq_prefix_name=""):
|
|
271
|
+
"""
|
|
272
|
+
Delete configured acquisition on the Resyst server.
|
|
273
|
+
If acq_prefix_name is not specified, all the acquisitons are removed.
|
|
274
|
+
"""
|
|
275
|
+
for acq in self._tg._program.acqs:
|
|
276
|
+
if acq.conf.name.startswith(acq_prefix_name):
|
|
277
|
+
self._program.remove_acq(acq.conf.name)
|
|
278
|
+
|
|
279
|
+
def _remove_finished_acqs(self):
|
|
280
|
+
"""
|
|
281
|
+
Used to remove all 'done' acquisitions.
|
|
282
|
+
"""
|
|
283
|
+
for acq in self._tg._program.acqs:
|
|
284
|
+
if acq.is_done:
|
|
285
|
+
self._program.remove_acq(acq.conf.name)
|
|
286
|
+
|
|
287
|
+
def _get_acq_from_name(self, name=None):
|
|
288
|
+
"""
|
|
289
|
+
Utility function to easily get the wanted acquisition (or the last loaded one if name is None)
|
|
290
|
+
"""
|
|
291
|
+
if name is None:
|
|
292
|
+
return self._acq # Get the last loaded Acquisition
|
|
293
|
+
else:
|
|
294
|
+
acq_dict = {acq.conf.name: acq for acq in self._program.acqs}
|
|
295
|
+
return acq_dict[name]
|
|
296
|
+
|
|
297
|
+
def _is_running(self, name=None):
|
|
298
|
+
"""
|
|
299
|
+
Returns whether the acquisition is currently acquiring data or not.
|
|
300
|
+
"""
|
|
301
|
+
acq = self._get_acq_from_name(name)
|
|
302
|
+
return acq.status.state == AcqState.RUNNING
|
|
303
|
+
|
|
304
|
+
def _is_stopped(self, name=None):
|
|
305
|
+
acq = self._get_acq_from_name(name)
|
|
306
|
+
return acq.status.state == AcqState.STOP
|
|
307
|
+
|
|
308
|
+
def _is_finished(self, name=None):
|
|
309
|
+
"""
|
|
310
|
+
This means that the Resyst server has received all the wanted data.
|
|
311
|
+
It is possible that the acquisition is stopped, but because not all data
|
|
312
|
+
has been received, _is_finished is False.
|
|
313
|
+
"""
|
|
314
|
+
return self._get_acq_from_name(name).is_done
|
|
315
|
+
|
|
316
|
+
def _wait_finished(self, silent=True, name=None):
|
|
317
|
+
"""
|
|
318
|
+
Blocking function that only returns when the acquisition is no longer running.
|
|
319
|
+
Could be because it has been manually stopped or because all data has been stored by the server.
|
|
320
|
+
"""
|
|
321
|
+
acq = self._get_acq_from_name(name)
|
|
322
|
+
with status_message(enable=not silent) as update:
|
|
323
|
+
while self._is_running(name):
|
|
324
|
+
update(
|
|
325
|
+
f"Waiting Acquisition to terminate ({acq.nb_sample_to_read}/{acq.conf.nbp})"
|
|
326
|
+
)
|
|
327
|
+
gevent.sleep(0.2)
|
rtint/rtint_counter.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# This file is part of the mechatronic project
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) Beamline Control Unit, ESRF
|
|
4
|
+
# Distributed under the GNU LGPLv3. See LICENSE for more info.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
REAL-TIME TARGET COUNTERS
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from tabulate import tabulate
|
|
12
|
+
|
|
13
|
+
RED = "\033[31m"
|
|
14
|
+
GREEN = "\033[32m"
|
|
15
|
+
RESET = "\033[0m"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RtintHdwCounterController:
|
|
19
|
+
def __init__(self, tg):
|
|
20
|
+
self._tg = tg
|
|
21
|
+
self._counters: dict[str, RtintHdwCounter] | None = None
|
|
22
|
+
self._load()
|
|
23
|
+
|
|
24
|
+
def __info__(self, debug=False):
|
|
25
|
+
"""Display list of all counters"""
|
|
26
|
+
if self._counters is None:
|
|
27
|
+
return "\n No Counter in the model"
|
|
28
|
+
|
|
29
|
+
if debug:
|
|
30
|
+
lines = [["Name", "Signal Path", "Value", "Unit", "Description"]]
|
|
31
|
+
else:
|
|
32
|
+
lines = [["Name", "Value", "Unit", "Description"]]
|
|
33
|
+
for counter in self._counters.values():
|
|
34
|
+
if debug:
|
|
35
|
+
lines.append(
|
|
36
|
+
[
|
|
37
|
+
counter.name,
|
|
38
|
+
counter.path,
|
|
39
|
+
counter._formated_value,
|
|
40
|
+
counter.unit,
|
|
41
|
+
counter.description,
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
lines.append(
|
|
46
|
+
[
|
|
47
|
+
counter.name,
|
|
48
|
+
counter._formated_value,
|
|
49
|
+
counter.unit,
|
|
50
|
+
counter.description,
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
return "\n" + tabulate(lines, headers="firstrow", tablefmt="grid", stralign="left")
|
|
54
|
+
|
|
55
|
+
def _load(self):
|
|
56
|
+
self._counters = {}
|
|
57
|
+
# Add custom counters defined in the YML file
|
|
58
|
+
counters_yml = self._tg._config.get("counters")
|
|
59
|
+
if counters_yml is not None:
|
|
60
|
+
for counter in counters_yml:
|
|
61
|
+
# Set defaults if not set in the YML file
|
|
62
|
+
counter.setdefault("description", None)
|
|
63
|
+
counter.setdefault("unit", None)
|
|
64
|
+
self._add_counter(
|
|
65
|
+
counter["name"],
|
|
66
|
+
counter["path"],
|
|
67
|
+
description=counter["description"],
|
|
68
|
+
unit=counter["unit"],
|
|
69
|
+
)
|
|
70
|
+
# Add counters defined in the Simulink file
|
|
71
|
+
pattern = re.compile(r"^(?P<name>.+?)_counter_$")
|
|
72
|
+
|
|
73
|
+
for signal_name, signal_obj in self._tg._program.tree.signals.items():
|
|
74
|
+
match = pattern.match(signal_obj.variable_name)
|
|
75
|
+
if not match:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
name = match.group("name")
|
|
79
|
+
|
|
80
|
+
counter_info = self._parse_signal_description(signal_obj.description)
|
|
81
|
+
|
|
82
|
+
# Unit corresponding to the counter
|
|
83
|
+
if "unit" in counter_info:
|
|
84
|
+
# Format unit because of forbiden characters in Simulink
|
|
85
|
+
unit = counter_info["unit"]
|
|
86
|
+
unit = unit.replace("_per_", "/")
|
|
87
|
+
unit = unit.replace("2", "^2")
|
|
88
|
+
unit = unit.replace("3", "^3")
|
|
89
|
+
else:
|
|
90
|
+
unit = None
|
|
91
|
+
|
|
92
|
+
# Description corresponding to the counter
|
|
93
|
+
if "description" in counter_info:
|
|
94
|
+
description = counter_info["description"]
|
|
95
|
+
else:
|
|
96
|
+
description = None
|
|
97
|
+
|
|
98
|
+
# Used to specify the display "format" of the counter
|
|
99
|
+
if "format_spec" in counter_info:
|
|
100
|
+
format_spec = counter_info["format_spec"]
|
|
101
|
+
else:
|
|
102
|
+
format_spec = None
|
|
103
|
+
|
|
104
|
+
self._add_counter(
|
|
105
|
+
name,
|
|
106
|
+
signal_obj.path[len(self._tg._program.name) + 1 :],
|
|
107
|
+
unit=unit,
|
|
108
|
+
description=description,
|
|
109
|
+
format_spec=format_spec,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _parse_signal_description(self, desc_str: str) -> dict:
|
|
113
|
+
"""
|
|
114
|
+
Parse a multiline description string containing lines like:
|
|
115
|
+
_desc: ...
|
|
116
|
+
_unit: ...
|
|
117
|
+
_disp: ...
|
|
118
|
+
Returns a dict without the leading underscores.
|
|
119
|
+
"""
|
|
120
|
+
result = {}
|
|
121
|
+
for line in desc_str.splitlines():
|
|
122
|
+
line = line.strip()
|
|
123
|
+
if not line or ":" not in line:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
key, value = line.split(":", 1)
|
|
127
|
+
key = key.strip().lstrip("_") # remove leading underscore
|
|
128
|
+
value = value.strip()
|
|
129
|
+
|
|
130
|
+
result[key] = value
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
def _add_counter(
|
|
135
|
+
self, name, path, description=None, unit=None, format_spec=None, force=False
|
|
136
|
+
):
|
|
137
|
+
# Verify signal path exists
|
|
138
|
+
try:
|
|
139
|
+
self._tg.signal.get(path)
|
|
140
|
+
except KeyError:
|
|
141
|
+
print(f"{RED}WARNING: Counter '{name}' has not a valid path{RESET}")
|
|
142
|
+
return
|
|
143
|
+
if force is False and name in self._counters:
|
|
144
|
+
print(
|
|
145
|
+
f"{RED}WARNING: Counter '{name}' already exists, use force=True to override{RESET}"
|
|
146
|
+
)
|
|
147
|
+
return
|
|
148
|
+
tg_counter = RtintHdwCounter(
|
|
149
|
+
self._tg,
|
|
150
|
+
name,
|
|
151
|
+
path,
|
|
152
|
+
description=description,
|
|
153
|
+
unit=unit,
|
|
154
|
+
format_spec=format_spec,
|
|
155
|
+
)
|
|
156
|
+
setattr(self, name, tg_counter)
|
|
157
|
+
self._counters[name] = tg_counter
|
|
158
|
+
|
|
159
|
+
def _get_counter_from_full_path(self, full_path):
|
|
160
|
+
for counter in self._counters.values():
|
|
161
|
+
if counter._full_path == full_path:
|
|
162
|
+
return counter
|
|
163
|
+
raise KeyError(f"No counter with full_path={full_path}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RtintHdwCounter:
|
|
167
|
+
"""Real-time target counter - Has name, description, unit and value"""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self, tg, name, path, description=None, unit=None, format_spec=None
|
|
171
|
+
):
|
|
172
|
+
self._tg = tg
|
|
173
|
+
self.name = name
|
|
174
|
+
self.path = path
|
|
175
|
+
self.description = description
|
|
176
|
+
self.unit = unit
|
|
177
|
+
self.format_spec = format_spec
|
|
178
|
+
|
|
179
|
+
def __info__(self):
|
|
180
|
+
lines = []
|
|
181
|
+
lines.append(["Name", self.name])
|
|
182
|
+
lines.append(["Description", self.description])
|
|
183
|
+
lines.append(["Unit", self.unit])
|
|
184
|
+
lines.append(["Path", self.path])
|
|
185
|
+
lines.append(["", ""])
|
|
186
|
+
lines.append(["Counter Value", self._formated_value])
|
|
187
|
+
return tabulate(lines, tablefmt="plain", stralign="right")
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def _full_path(self):
|
|
191
|
+
return f"{self._tg._program.name}/{self.path}"
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def value(self):
|
|
195
|
+
return self._tg.signal.get(self.path)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def _formated_value(self):
|
|
199
|
+
if self.format_spec is None:
|
|
200
|
+
return repr(self.value)
|
|
201
|
+
else:
|
|
202
|
+
return f"{self.value:{self.format_spec}}"
|
rtint/rtint_ethercat.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of the mechatronic project
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) Beamline Control Unit, ESRF
|
|
6
|
+
# Distributed under the GNU LGPLv3. See LICENSE for more info.
|
|
7
|
+
|
|
8
|
+
import enum
|
|
9
|
+
import gevent
|
|
10
|
+
import time
|
|
11
|
+
from tabulate import tabulate
|
|
12
|
+
|
|
13
|
+
from rtint.rtint_utils import status_message
|
|
14
|
+
|
|
15
|
+
RED = "\033[31m"
|
|
16
|
+
GREEN = "\033[32m"
|
|
17
|
+
RESET = "\033[0m"
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
REAL-TIME TARGET EtherCAT
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EthercatState(enum.IntEnum):
|
|
25
|
+
INIT = 1
|
|
26
|
+
PREOP = 2
|
|
27
|
+
SAFEOP = 4
|
|
28
|
+
OP = 8
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RtintHdwEthercatController:
|
|
32
|
+
def __init__(self, tg):
|
|
33
|
+
self._tg = tg
|
|
34
|
+
self._ethercats: dict[str, RtintHdwEthercat] | None = None
|
|
35
|
+
self._load()
|
|
36
|
+
|
|
37
|
+
def __info__(self, debug=False):
|
|
38
|
+
if self._ethercats is None:
|
|
39
|
+
return "\n No EtherCAT network in the model"
|
|
40
|
+
|
|
41
|
+
if debug:
|
|
42
|
+
lines = [["Name", "Path", "State"]]
|
|
43
|
+
else:
|
|
44
|
+
lines = [["Name", "State"]]
|
|
45
|
+
|
|
46
|
+
for _ethercat in self._ethercats.values():
|
|
47
|
+
if debug:
|
|
48
|
+
lines.append(
|
|
49
|
+
[
|
|
50
|
+
_ethercat._name,
|
|
51
|
+
_ethercat._unique_name,
|
|
52
|
+
EthercatState(_ethercat.state).name,
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
lines.append([_ethercat._name, EthercatState(_ethercat.state).name])
|
|
57
|
+
return "\n" + tabulate(lines, headers="firstrow", tablefmt="grid", stralign="left")
|
|
58
|
+
|
|
59
|
+
def _load(self):
|
|
60
|
+
ethercats = self._tg._get_all_objects_from_key("bliss_ethercat")
|
|
61
|
+
if len(ethercats) > 0:
|
|
62
|
+
self._ethercats = {}
|
|
63
|
+
for ethercat in ethercats:
|
|
64
|
+
tg_ethercat = RtintHdwEthercat(self._tg, ethercat)
|
|
65
|
+
|
|
66
|
+
if hasattr(self, tg_ethercat._name):
|
|
67
|
+
print(
|
|
68
|
+
f"{RED}WARNING: ethercat '{tg_ethercat._name}' already exists{RESET}"
|
|
69
|
+
)
|
|
70
|
+
return
|
|
71
|
+
else:
|
|
72
|
+
setattr(self, tg_ethercat._name, tg_ethercat)
|
|
73
|
+
self._ethercats[tg_ethercat._name] = tg_ethercat
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RtintHdwEthercat:
|
|
77
|
+
def __init__(self, tg, unique_name):
|
|
78
|
+
self._tg = tg
|
|
79
|
+
self._unique_name = unique_name
|
|
80
|
+
|
|
81
|
+
def __info__(self):
|
|
82
|
+
lines = []
|
|
83
|
+
lines.append(["Name", self._name])
|
|
84
|
+
lines.append(["Unique Name", self._unique_name])
|
|
85
|
+
lines.append(["", ""])
|
|
86
|
+
lines.append(["State", EthercatState(self.state).name])
|
|
87
|
+
return tabulate(lines, tablefmt="plain", stralign="right")
|
|
88
|
+
|
|
89
|
+
def _tree(self):
|
|
90
|
+
print("Parameters:")
|
|
91
|
+
self._tg.parameter._tree.subtree(
|
|
92
|
+
self._tg._program.name + "/" + self._unique_name
|
|
93
|
+
).show()
|
|
94
|
+
print("Signals:")
|
|
95
|
+
self._tg.signal._tree.subtree(
|
|
96
|
+
self._tg._program.name + "/" + self._unique_name
|
|
97
|
+
).show()
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def _name(self):
|
|
101
|
+
return self._tg.parameter.get(
|
|
102
|
+
f"{self._unique_name}/bliss_ethercat/String"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def state(self):
|
|
107
|
+
return self._tg.signal.get(f"{self._unique_name}/MasterState")
|
|
108
|
+
|
|
109
|
+
def set_state(self, state, wait=False, display=False, timeout=10):
|
|
110
|
+
self._tg.parameter.set(f"{self._unique_name}/wanted_state/Value", state)
|
|
111
|
+
self._tg.parameter.set(
|
|
112
|
+
f"{self._unique_name}/state_trigger/Bias",
|
|
113
|
+
self._tg.parameter.get(f"{self._unique_name}/state_trigger/Bias")
|
|
114
|
+
+ 1,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if wait is True:
|
|
118
|
+
start_time = time.time() # Here we suppose the acquisition has just started
|
|
119
|
+
with status_message(enable=display) as update:
|
|
120
|
+
while self.state != state:
|
|
121
|
+
if display is True:
|
|
122
|
+
update(f"Current State: {EthercatState(self.state).name}")
|
|
123
|
+
gevent.sleep(0.2)
|
|
124
|
+
if time.time() - start_time > timeout:
|
|
125
|
+
raise TimeoutError("Timeout while changing the EtherCAT State")
|
|
126
|
+
if display is True:
|
|
127
|
+
update(f"Current State: {EthercatState(self.state).name}")
|