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 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}}"
@@ -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}")