genie-python 15.1.0rc1__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.
- genie_python/.pylintrc +539 -0
- genie_python/__init__.py +1 -0
- genie_python/block_names.py +123 -0
- genie_python/channel_access_exceptions.py +45 -0
- genie_python/genie.py +2462 -0
- genie_python/genie_advanced.py +418 -0
- genie_python/genie_alerts.py +195 -0
- genie_python/genie_api_setup.py +451 -0
- genie_python/genie_blockserver.py +64 -0
- genie_python/genie_cachannel_wrapper.py +545 -0
- genie_python/genie_change_cache.py +151 -0
- genie_python/genie_dae.py +2218 -0
- genie_python/genie_epics_api.py +906 -0
- genie_python/genie_experimental_data.py +186 -0
- genie_python/genie_logging.py +200 -0
- genie_python/genie_p4p_wrapper.py +203 -0
- genie_python/genie_plot.py +77 -0
- genie_python/genie_pre_post_cmd_manager.py +21 -0
- genie_python/genie_pv_connection_protocol.py +36 -0
- genie_python/genie_script_checker.py +507 -0
- genie_python/genie_script_generator.py +212 -0
- genie_python/genie_simulate.py +69 -0
- genie_python/genie_simulate_impl.py +1265 -0
- genie_python/genie_startup.py +29 -0
- genie_python/genie_toggle_settings.py +58 -0
- genie_python/genie_wait_for_move.py +154 -0
- genie_python/genie_waitfor.py +576 -0
- genie_python/matplotlib_backend/__init__.py +0 -0
- genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
- genie_python/mysql_abstraction_layer.py +272 -0
- genie_python/run_tests.py +56 -0
- genie_python/scanning_instrument_pylint_plugin.py +31 -0
- genie_python/typings/CaChannel/CaChannel.pyi +893 -0
- genie_python/typings/CaChannel/__init__.pyi +9 -0
- genie_python/typings/CaChannel/_version.pyi +6 -0
- genie_python/typings/CaChannel/ca.pyi +31 -0
- genie_python/utilities.py +406 -0
- genie_python/version.py +1 -0
- genie_python-15.1.0rc1.dist-info/LICENSE +28 -0
- genie_python-15.1.0rc1.dist-info/METADATA +95 -0
- genie_python-15.1.0rc1.dist-info/RECORD +43 -0
- genie_python-15.1.0rc1.dist-info/WHEEL +5 -0
- genie_python-15.1.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wrapping of channel access in genie_python
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import absolute_import, print_function
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
from builtins import object
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from threading import Event
|
|
12
|
+
from typing import TYPE_CHECKING, Optional, Tuple, TypeVar
|
|
13
|
+
|
|
14
|
+
from CaChannel import CaChannel, CaChannelException, ca
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from CaChannel._ca import (
|
|
18
|
+
AlarmCondition,
|
|
19
|
+
AlarmSeverity,
|
|
20
|
+
dbf_type_to_DBR_STS,
|
|
21
|
+
dbf_type_to_DBR_TIME,
|
|
22
|
+
)
|
|
23
|
+
except ImportError:
|
|
24
|
+
from caffi.ca import AlarmCondition, AlarmSeverity, dbf_type_to_DBR_STS, dbf_type_to_DBR_TIME
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from genie_python.genie import PVValue
|
|
28
|
+
|
|
29
|
+
from .channel_access_exceptions import (
|
|
30
|
+
InvalidEnumStringException,
|
|
31
|
+
ReadAccessException,
|
|
32
|
+
UnableToConnectToPVException,
|
|
33
|
+
WriteAccessException,
|
|
34
|
+
)
|
|
35
|
+
from .utilities import waveform_to_string
|
|
36
|
+
|
|
37
|
+
TIMEOUT = 15 # Default timeout for PV set/get
|
|
38
|
+
EXIST_TIMEOUT = 3 # Separate smaller timeout for pv_exists() and searchw() operations
|
|
39
|
+
CACHE = threading.local()
|
|
40
|
+
CACHE_LOCK = threading.local()
|
|
41
|
+
T = TypeVar("T")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CaChannelWrapper(object):
|
|
45
|
+
"""
|
|
46
|
+
Wrap CA Channel access to give utilities methods for access in one place
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
error_log_func: Optional[Callable[[str], None]] = None
|
|
50
|
+
|
|
51
|
+
# noinspection PyPep8Naming
|
|
52
|
+
@staticmethod
|
|
53
|
+
def logError(message: str): # noqa N802
|
|
54
|
+
"""
|
|
55
|
+
Log an error
|
|
56
|
+
Args:
|
|
57
|
+
message: message to log
|
|
58
|
+
"""
|
|
59
|
+
if CaChannelWrapper.error_log_func is not None:
|
|
60
|
+
try:
|
|
61
|
+
CaChannelWrapper.error_log_func(message)
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
else:
|
|
65
|
+
print("CAERROR: {}".format(message))
|
|
66
|
+
|
|
67
|
+
# noinspection PyPep8Naming
|
|
68
|
+
@staticmethod
|
|
69
|
+
def printfHandler(message: str, user_args: Tuple[T, ...]) -> None: # noqa N802
|
|
70
|
+
"""
|
|
71
|
+
Callback used for CA printing messages.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
message (string): Contains the results of the action.
|
|
75
|
+
user_args (tuple): Contains any extra arguments supplied to the call.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
None.
|
|
79
|
+
"""
|
|
80
|
+
CaChannelWrapper.logError("CAMessage: {}".format(message))
|
|
81
|
+
|
|
82
|
+
# noinspection PyPep8Naming
|
|
83
|
+
@staticmethod
|
|
84
|
+
def CAExceptionHandler(epics_args: dict[str, str], user_args: Tuple[None]) -> None: # noqa N802
|
|
85
|
+
"""
|
|
86
|
+
Callback used for CA exception messages.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
epics_args (dict): Contains the results of the action - see C struct
|
|
90
|
+
"exception_handler_args"
|
|
91
|
+
Available ones are: chid, type, count, state, op, ctx, file, lineNo
|
|
92
|
+
|
|
93
|
+
user_args (dict): Contains any extra arguments supplied to the call.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
None.
|
|
97
|
+
"""
|
|
98
|
+
CaChannelWrapper.logError(
|
|
99
|
+
"CAException: ctx={} type={} state={} op={} file={} lineNo={}".format(
|
|
100
|
+
epics_args["ctx"],
|
|
101
|
+
epics_args["type"],
|
|
102
|
+
epics_args["state"],
|
|
103
|
+
epics_args["op"],
|
|
104
|
+
epics_args["file"],
|
|
105
|
+
epics_args["lineNo"],
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# noinspection PyPep8Naming
|
|
110
|
+
@staticmethod
|
|
111
|
+
def installHandlers(chan: CaChannel) -> None: # noqa N802
|
|
112
|
+
"""
|
|
113
|
+
Installs callbacks for printf and exceptions.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
chan: CaChannel instance
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
None.
|
|
120
|
+
"""
|
|
121
|
+
# We do a poll() so ca_context_create() gets called with arguments to enable preemptive
|
|
122
|
+
# callbacks CaChannel itself delays creation of the context, so if we just installed the
|
|
123
|
+
# handlers now we would get a default non-preemptive CA context created.
|
|
124
|
+
chan.poll()
|
|
125
|
+
try:
|
|
126
|
+
chan.replace_printf_handler(CaChannelWrapper.printfHandler)
|
|
127
|
+
except AttributeError:
|
|
128
|
+
# If we can't replace the printf handler, ignore that error - it is not crucial.
|
|
129
|
+
# It probably means we are using default CaChannel, as opposed to ISIS' special build.
|
|
130
|
+
# Cope with both cases.
|
|
131
|
+
pass
|
|
132
|
+
chan.add_exception_event(CaChannelWrapper.CAExceptionHandler)
|
|
133
|
+
|
|
134
|
+
# noinspection PyPep8Naming
|
|
135
|
+
@staticmethod
|
|
136
|
+
def putCB(epics_args: Tuple[str, int, int, int], user_args: Tuple[Event, ...]) -> None: # noqa N802
|
|
137
|
+
"""
|
|
138
|
+
Callback used for setting PV values.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
epics_args (tuple): Contains the results of the action.
|
|
142
|
+
user_args (tuple): Contains any extra arguments supplied to the call.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
None.
|
|
146
|
+
"""
|
|
147
|
+
user_args[0].set()
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def set_pv_value(
|
|
151
|
+
name: str,
|
|
152
|
+
value: "PVValue",
|
|
153
|
+
wait: bool = False,
|
|
154
|
+
timeout: float = TIMEOUT,
|
|
155
|
+
safe_not_quick: bool = True,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Set the PV to a value.
|
|
159
|
+
|
|
160
|
+
When getting a PV value this call should be used, unless there is a special requirement.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
name (string): The PV name.
|
|
164
|
+
value: The value to set.
|
|
165
|
+
wait (bool, optional): Wait for the value to be set before returning.
|
|
166
|
+
timeout (optional): How long to wait for the PV to connect etc.
|
|
167
|
+
safe_not_quick (bool): True run all checks while setting the pv, False don't run checks
|
|
168
|
+
just write the value, e.g. disp check
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
None.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
UnableToConnectToPVException: If cannot connect to PV.
|
|
175
|
+
WriteAccessException: If write access is denied.
|
|
176
|
+
InvalidEnumStringException: If the PV is an enum and the string value supplied is not a
|
|
177
|
+
valid enum value.
|
|
178
|
+
"""
|
|
179
|
+
chan = CaChannelWrapper.get_chan(name)
|
|
180
|
+
chan.setTimeout(timeout)
|
|
181
|
+
|
|
182
|
+
# Validate user input and format accordingly for mbbi/bi records
|
|
183
|
+
value = CaChannelWrapper.check_for_enum_value(value, chan, name)
|
|
184
|
+
|
|
185
|
+
if not chan.write_access():
|
|
186
|
+
raise WriteAccessException(name)
|
|
187
|
+
if safe_not_quick:
|
|
188
|
+
CaChannelWrapper._check_for_disp(name)
|
|
189
|
+
if wait:
|
|
190
|
+
ftype = chan.field_type()
|
|
191
|
+
ecount = chan.element_count()
|
|
192
|
+
event = Event()
|
|
193
|
+
chan.array_put_callback(value, ftype, ecount, CaChannelWrapper.putCB, event)
|
|
194
|
+
CaChannelWrapper._wait_for_pend_event(chan, event, timeout=None)
|
|
195
|
+
else:
|
|
196
|
+
# putw() flushes send buffer, but doesn't wait for a CA completion callback
|
|
197
|
+
# Write value to PV, or produce error
|
|
198
|
+
chan.putw(value)
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _check_for_disp(name: str) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Check if DISP is set on a PV. If passed a field instead of a PV, do nothing.
|
|
204
|
+
Only check DISP if it exists.
|
|
205
|
+
"""
|
|
206
|
+
if (
|
|
207
|
+
".DISP" not in name
|
|
208
|
+
): # Do not check for DISP if it's already in the name of the PV to check
|
|
209
|
+
if "." in name: # If given a field on a PV, check the PV itself if DISP is set
|
|
210
|
+
name = name.split(".")[0]
|
|
211
|
+
_disp_name = "{}.DISP".format(name)
|
|
212
|
+
if (
|
|
213
|
+
CaChannelWrapper.pv_exists(_disp_name, 0)
|
|
214
|
+
and CaChannelWrapper.get_pv_value(_disp_name) != "0"
|
|
215
|
+
):
|
|
216
|
+
raise WriteAccessException("{} (DISP is set)".format(name))
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def get_chan(name: str, timeout: float = EXIST_TIMEOUT) -> CaChannel:
|
|
220
|
+
"""
|
|
221
|
+
Gets a channel based on a channel name, from the cache if it exists.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
name: the name of the channel to get
|
|
225
|
+
timeout: timeout to set on channel
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
CaChannel object representing the channel
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
UnableToConnectToPVException if it was unable to connect to the channel
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
lock = CACHE_LOCK.lock
|
|
236
|
+
except AttributeError:
|
|
237
|
+
lock = CACHE_LOCK.lock = threading.RLock()
|
|
238
|
+
|
|
239
|
+
with lock:
|
|
240
|
+
try:
|
|
241
|
+
pv_map = CACHE.map
|
|
242
|
+
except AttributeError:
|
|
243
|
+
pv_map = CACHE.map = {}
|
|
244
|
+
|
|
245
|
+
if name in list(pv_map.keys()) and pv_map[name].state() == ca.cs_conn:
|
|
246
|
+
chan = pv_map[name]
|
|
247
|
+
else:
|
|
248
|
+
chan = CaChannel(name)
|
|
249
|
+
# noinspection PyTypeChecker
|
|
250
|
+
CaChannelWrapper.installHandlers(chan)
|
|
251
|
+
chan.setTimeout(timeout)
|
|
252
|
+
# Try to connect - throws if cannot
|
|
253
|
+
CaChannelWrapper.connect_to_pv(chan)
|
|
254
|
+
pv_map[name] = chan
|
|
255
|
+
return chan
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def clear_monitor(name: str, timeout: float = EXIST_TIMEOUT) -> None:
|
|
259
|
+
channel = CaChannelWrapper.get_chan(name, timeout)
|
|
260
|
+
channel.clear_channel()
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def get_pv_value(
|
|
264
|
+
name: str, to_string: bool = False, timeout: float = TIMEOUT, use_numpy: bool | None = None
|
|
265
|
+
) -> "PVValue":
|
|
266
|
+
"""
|
|
267
|
+
Get the current value of the PV.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
name (name): The PV.
|
|
271
|
+
to_string (bool, optional): Whether to convert the value to a string.
|
|
272
|
+
timeout (optional): How long to wait for the PV to connect etc.
|
|
273
|
+
use_numpy (None|boolean): True use numpy to return arrays, False return a list;
|
|
274
|
+
None for use the default
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
The PV value.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
UnableToConnectToPVException: If cannot connect to PV.
|
|
281
|
+
ReadAccessException: If read access is denied.
|
|
282
|
+
"""
|
|
283
|
+
chan = CaChannelWrapper.get_chan(name)
|
|
284
|
+
chan.setTimeout(timeout)
|
|
285
|
+
if not chan.read_access():
|
|
286
|
+
raise ReadAccessException(name)
|
|
287
|
+
ftype = chan.field_type()
|
|
288
|
+
if ca.dbr_type_is_ENUM(ftype) or ca.dbr_type_is_CHAR(ftype) or ca.dbr_type_is_STRING(ftype):
|
|
289
|
+
to_string = True
|
|
290
|
+
if to_string:
|
|
291
|
+
if ca.dbr_type_is_ENUM(ftype) or ca.dbr_type_is_STRING(ftype):
|
|
292
|
+
value = chan.getw(ca.DBR_STRING)
|
|
293
|
+
else:
|
|
294
|
+
# If we get a numeric using ca.DBR_CHAR the value still comes back as a numeric
|
|
295
|
+
# In other words, it does not get cast to char
|
|
296
|
+
value = chan.getw(ca.DBR_CHAR)
|
|
297
|
+
# Could see if the element count is > 1 instead
|
|
298
|
+
if isinstance(value, list):
|
|
299
|
+
return waveform_to_string(value)
|
|
300
|
+
else:
|
|
301
|
+
return str(value)
|
|
302
|
+
else:
|
|
303
|
+
if use_numpy is None:
|
|
304
|
+
output = chan.getw()
|
|
305
|
+
else:
|
|
306
|
+
output = chan.getw(use_numpy=use_numpy)
|
|
307
|
+
assert not isinstance(output, dict)
|
|
308
|
+
return output
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def get_pv_timestamp(name: str, timeout: float = TIMEOUT) -> Tuple[int, int]:
|
|
312
|
+
"""
|
|
313
|
+
Get the timestamp of when the PV was last processed.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
name (name): The PV.
|
|
317
|
+
timeout (optional): How long to wait for the PV to connect etc.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
tuple of: (seconds, nanoseconds)
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
UnableToConnectToPVException: If cannot connect to PV.
|
|
324
|
+
ReadAccessException: If read access is denied.
|
|
325
|
+
"""
|
|
326
|
+
chan = CaChannelWrapper.get_chan(name)
|
|
327
|
+
chan.setTimeout(timeout)
|
|
328
|
+
if not chan.read_access():
|
|
329
|
+
raise ReadAccessException(name)
|
|
330
|
+
ftype = chan.field_type()
|
|
331
|
+
info = chan.getw(dbf_type_to_DBR_TIME(ftype))
|
|
332
|
+
assert isinstance(info, dict)
|
|
333
|
+
return info["pv_seconds"], info["pv_nseconds"]
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
def pv_exists(name: str, timeout: float = EXIST_TIMEOUT) -> bool:
|
|
337
|
+
"""
|
|
338
|
+
See if the PV exists.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
name (string): The PV name.
|
|
342
|
+
timeout(optional): How long to wait for the PV to "appear".
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
True if exists, otherwise False.
|
|
346
|
+
"""
|
|
347
|
+
try:
|
|
348
|
+
chan = CaChannelWrapper.get_chan(name, timeout)
|
|
349
|
+
CaChannelWrapper.connect_to_pv(chan)
|
|
350
|
+
return True
|
|
351
|
+
except UnableToConnectToPVException:
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def connect_to_pv(ca_channel: CaChannel) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Connects to the PV.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
ca_channel (CaChannel): The channel to connect to.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
None.
|
|
364
|
+
|
|
365
|
+
Raises:
|
|
366
|
+
UnableToConnectToPVException: If cannot connect to PV.
|
|
367
|
+
"""
|
|
368
|
+
if os.getenv("GITHUB_ACTIONS"):
|
|
369
|
+
# genie_python does some PV accesses on import. To avoid them timing out and making CI
|
|
370
|
+
# builds really slow, shortcut every PV to "non-existent" here.
|
|
371
|
+
raise UnableToConnectToPVException("", "In CI")
|
|
372
|
+
|
|
373
|
+
event = Event()
|
|
374
|
+
try:
|
|
375
|
+
ca_channel.search_and_connect(None, CaChannelWrapper.putCB, event)
|
|
376
|
+
except CaChannelException as e:
|
|
377
|
+
raise UnableToConnectToPVException(ca_channel.name(), e)
|
|
378
|
+
|
|
379
|
+
ca_channel.flush_io()
|
|
380
|
+
|
|
381
|
+
# we do not need to call pend_event / poll as we are using preemptive callbacks
|
|
382
|
+
time_elapsed = 0.0
|
|
383
|
+
interval = 0.1
|
|
384
|
+
while True:
|
|
385
|
+
time_elapsed += interval
|
|
386
|
+
if event.wait(interval) or time_elapsed >= ca_channel.getTimeout():
|
|
387
|
+
break
|
|
388
|
+
|
|
389
|
+
if not event.is_set():
|
|
390
|
+
raise UnableToConnectToPVException(ca_channel.name(), "Connection timeout (event)")
|
|
391
|
+
|
|
392
|
+
if ca_channel.state() != ca.cs_conn:
|
|
393
|
+
raise UnableToConnectToPVException(ca_channel.name(), "Connection timeout (state)")
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def check_for_enum_value(value: "PVValue", chan: CaChannel, name: str) -> "PVValue":
|
|
397
|
+
"""
|
|
398
|
+
Check for string input for MBBI/BI records and replace with the equivalent index value.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
value: The PV value.
|
|
402
|
+
chan (CaChannel): The channel access channel.
|
|
403
|
+
name (string): The name of the channel.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Index value of enum, if the record is mbbi/bi. Otherwise, returns unmodified value.
|
|
407
|
+
|
|
408
|
+
Raises:
|
|
409
|
+
InvalidEnumStringException: If the string supplied is not a valid enum value.
|
|
410
|
+
"""
|
|
411
|
+
# If PV is MBBI/BI type, search list of enum values and iterate to find a match
|
|
412
|
+
if ca.dbr_type_is_ENUM(chan.field_type()) and isinstance(value, str):
|
|
413
|
+
chan.array_get(ca.DBR_CTRL_ENUM)
|
|
414
|
+
chan.pend_io()
|
|
415
|
+
channel_properties = chan.getValue()
|
|
416
|
+
for index, enum_value in enumerate(channel_properties["pv_statestrings"]):
|
|
417
|
+
if enum_value.lower() == value.lower():
|
|
418
|
+
# Replace user input with enum index value
|
|
419
|
+
return index
|
|
420
|
+
# If the string entered isn't valid then throw
|
|
421
|
+
raise InvalidEnumStringException(name, channel_properties["pv_statestrings"])
|
|
422
|
+
|
|
423
|
+
return value
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
def add_monitor(
|
|
427
|
+
name: str,
|
|
428
|
+
call_back_function: "Callable[[PVValue, str, str], None]",
|
|
429
|
+
link_alarm_on_disconnect: bool = True,
|
|
430
|
+
to_string: bool = False,
|
|
431
|
+
use_numpy: bool | None = None,
|
|
432
|
+
) -> Callable[[], None]:
|
|
433
|
+
"""
|
|
434
|
+
Add a callback to a pv which responds on a monitor (i.e. value change).
|
|
435
|
+
This currently only tested for numbers.
|
|
436
|
+
Args:
|
|
437
|
+
name: name of the pv
|
|
438
|
+
call_back_function: the callback function, arguments are value, alarm severity
|
|
439
|
+
(CaChannel._ca.AlarmSeverity), alarm status (CaChannel._ca.AlarmCondition)
|
|
440
|
+
link_alarm_on_disconnect: if set to True, a link alarm is sent with the last value
|
|
441
|
+
when the pv disconnects
|
|
442
|
+
use_numpy (bool, optional): True use numpy to return arrays,
|
|
443
|
+
False return a list; None for use the default
|
|
444
|
+
Returns:
|
|
445
|
+
unsubscribe event function
|
|
446
|
+
"""
|
|
447
|
+
from CaChannel import USE_NUMPY
|
|
448
|
+
|
|
449
|
+
if use_numpy is None:
|
|
450
|
+
use_numpy = USE_NUMPY
|
|
451
|
+
chan = CaChannelWrapper.get_chan(name)
|
|
452
|
+
if not chan.read_access():
|
|
453
|
+
raise ReadAccessException(name)
|
|
454
|
+
field_type = chan.field_type()
|
|
455
|
+
# if this is an enum field return the monitor as a string (not an int)
|
|
456
|
+
if ca.dbr_type_is_ENUM(field_type):
|
|
457
|
+
field_type = ca.DBR_STRING
|
|
458
|
+
# Modify the field type from monitor the value to includes the alarm severity and status
|
|
459
|
+
field_type_with_status = dbf_type_to_DBR_STS(field_type)
|
|
460
|
+
|
|
461
|
+
def _process_call_back(epics_args: dict[str, str], _: dict[str, str]) -> None:
|
|
462
|
+
value = epics_args.get("pv_value", None)
|
|
463
|
+
|
|
464
|
+
if to_string:
|
|
465
|
+
# Could see if the element count is > 1 instead
|
|
466
|
+
if isinstance(value, list):
|
|
467
|
+
value = waveform_to_string(value)
|
|
468
|
+
else:
|
|
469
|
+
value = str(value)
|
|
470
|
+
|
|
471
|
+
chan.last_value = value
|
|
472
|
+
call_back_function(
|
|
473
|
+
value,
|
|
474
|
+
epics_args.get("pv_severity", AlarmSeverity.No),
|
|
475
|
+
epics_args.get("pv_status", AlarmCondition.No),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def _connection_callback(epics_args: Tuple[T, ...], _: dict[str, str]) -> None:
|
|
479
|
+
if epics_args[1] == ca.CA_OP_CONN_DOWN:
|
|
480
|
+
call_back_function(chan.last_value, AlarmSeverity.Invalid, AlarmCondition.Link)
|
|
481
|
+
|
|
482
|
+
chan.add_masked_array_event(
|
|
483
|
+
field_type_with_status,
|
|
484
|
+
count=None,
|
|
485
|
+
mask=None,
|
|
486
|
+
callback=_process_call_back,
|
|
487
|
+
use_numpy=use_numpy,
|
|
488
|
+
)
|
|
489
|
+
if link_alarm_on_disconnect:
|
|
490
|
+
chan.change_connection_event(_connection_callback)
|
|
491
|
+
|
|
492
|
+
return chan.clear_event
|
|
493
|
+
|
|
494
|
+
@staticmethod
|
|
495
|
+
def poll() -> None:
|
|
496
|
+
"""
|
|
497
|
+
Flush the send buffer and execute any outstanding background activity for all connected pvs.
|
|
498
|
+
NB Connected pv is one which is in the cache
|
|
499
|
+
"""
|
|
500
|
+
# pick first channel and perform flush on it.
|
|
501
|
+
try:
|
|
502
|
+
for key, value in CACHE.map.items():
|
|
503
|
+
value.poll()
|
|
504
|
+
break
|
|
505
|
+
except AttributeError:
|
|
506
|
+
# There are no channels so we do not need to poll them
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
@staticmethod
|
|
510
|
+
def _wait_for_pend_event(
|
|
511
|
+
chan: CaChannel, event: Event, timeout: Optional[float] = None, interval: float = 0.1
|
|
512
|
+
) -> None:
|
|
513
|
+
"""
|
|
514
|
+
Wait for a pending event to occur in short intervals to allow for keyboard interrupt;
|
|
515
|
+
has possible timeout for maximum time to wait. This should be used for put operation
|
|
516
|
+
callbacks.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
chan: channel to use
|
|
520
|
+
event: the event posted by the callback to wait for
|
|
521
|
+
timeout: maximum time to wait for the event, None means wait forever.
|
|
522
|
+
interval: time to poll channel access
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
time_elapsed = 0
|
|
526
|
+
|
|
527
|
+
while True:
|
|
528
|
+
# Should use overall timeout somehow? need to make sure it is long enough for
|
|
529
|
+
# all requests to complete did try flush_io() followed by event.wait(1.0) inside the
|
|
530
|
+
# loop for set pv, but a send got missed (this is what util/caput in CaChannel does with
|
|
531
|
+
# its wait is set to True)So looks like pend_event() / pend_io() / poll() is needed
|
|
532
|
+
# CaChannel example uses pend_event, pyepics seems to do both pend_io and pend_event
|
|
533
|
+
# According to docs, if using preemptive callbacks then only an initial flush_io()
|
|
534
|
+
# should be needed
|
|
535
|
+
|
|
536
|
+
status = chan.poll() # equivalent to pend_event() with a small timeout
|
|
537
|
+
if status != ca.ECA_TIMEOUT:
|
|
538
|
+
raise CaChannelException(status)
|
|
539
|
+
|
|
540
|
+
time_elapsed += interval
|
|
541
|
+
if event.wait(interval) or (timeout is not None and time_elapsed >= timeout):
|
|
542
|
+
break
|
|
543
|
+
|
|
544
|
+
if not event.is_set():
|
|
545
|
+
raise UnableToConnectToPVException(chan.name(), "Pend event timeout")
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from builtins import object, str
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ChangeCache(object):
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.wiring = None
|
|
7
|
+
self.detector = None
|
|
8
|
+
self.spectra = None
|
|
9
|
+
self.mon_spect = None
|
|
10
|
+
self.mon_from = None
|
|
11
|
+
self.mon_to = None
|
|
12
|
+
self.dae_sync = None
|
|
13
|
+
self.tcb_file = None
|
|
14
|
+
self.tcb_tables = []
|
|
15
|
+
self.tcb_calculation_method = None
|
|
16
|
+
self.smp_veto = None
|
|
17
|
+
self.ts2_veto = None
|
|
18
|
+
self.hz50_veto = None
|
|
19
|
+
self.ext0_veto = None
|
|
20
|
+
self.ext1_veto = None
|
|
21
|
+
self.ext2_veto = None
|
|
22
|
+
self.ext3_veto = None
|
|
23
|
+
self.fermi_veto = None
|
|
24
|
+
self.fermi_delay = None
|
|
25
|
+
self.fermi_width = None
|
|
26
|
+
self.periods_soft_num = None
|
|
27
|
+
self.periods_type = None
|
|
28
|
+
self.periods_src = None
|
|
29
|
+
self.periods_file = None
|
|
30
|
+
self.periods_seq = None
|
|
31
|
+
self.periods_delay = None
|
|
32
|
+
self.periods_settings = []
|
|
33
|
+
|
|
34
|
+
def set_monitor(self, spec, low, high):
|
|
35
|
+
self.mon_spect = spec
|
|
36
|
+
self.mon_from = low
|
|
37
|
+
self.mon_to = high
|
|
38
|
+
|
|
39
|
+
def clear_vetos(self):
|
|
40
|
+
self.smp_veto = 0
|
|
41
|
+
self.ts2_veto = 0
|
|
42
|
+
self.hz50_veto = 0
|
|
43
|
+
self.ext0_veto = 0
|
|
44
|
+
self.ext1_veto = 0
|
|
45
|
+
self.ext2_veto = 0
|
|
46
|
+
self.ext3_veto = 0
|
|
47
|
+
|
|
48
|
+
def set_fermi(self, enable, delay=1.0, width=1.0):
|
|
49
|
+
self.fermi_veto = 1 if enable else 0
|
|
50
|
+
self.fermi_delay = delay
|
|
51
|
+
self.fermi_width = width
|
|
52
|
+
|
|
53
|
+
def change_dae_settings(self, root):
|
|
54
|
+
changed = self._change_xml(root, "String", "Wiring Table", self.wiring)
|
|
55
|
+
changed |= self._change_xml(root, "String", "Detector Table", self.detector)
|
|
56
|
+
changed |= self._change_xml(root, "String", "Spectra Table", self.spectra)
|
|
57
|
+
changed |= self._change_xml(root, "I32", "Monitor Spectrum", self.mon_spect)
|
|
58
|
+
changed |= self._change_xml(root, "DBL", "from", self.mon_from)
|
|
59
|
+
changed |= self._change_xml(root, "DBL", "to", self.mon_to)
|
|
60
|
+
changed |= self._change_xml(root, "EW", "DAETimingSource", self.dae_sync)
|
|
61
|
+
|
|
62
|
+
if self.fermi_veto is not None:
|
|
63
|
+
self._change_xml(root, "EW", " Fermi Chopper Veto", self.fermi_veto)
|
|
64
|
+
self._change_xml(root, "DBL", "FC Delay", self.fermi_delay)
|
|
65
|
+
self._change_xml(root, "DBL", "FC Width", self.fermi_width)
|
|
66
|
+
changed |= True
|
|
67
|
+
|
|
68
|
+
changed |= self._change_vetos(root)
|
|
69
|
+
return changed
|
|
70
|
+
|
|
71
|
+
def _change_vetos(self, root):
|
|
72
|
+
changed = self._change_xml(root, "EW", "SMP (Chopper) Veto", self.smp_veto)
|
|
73
|
+
changed |= self._change_xml(root, "EW", " TS2 Pulse Veto", self.ts2_veto)
|
|
74
|
+
changed |= self._change_xml(root, "EW", " ISIS 50Hz Veto", self.hz50_veto)
|
|
75
|
+
changed |= self._change_xml(root, "EW", "Veto 0", self.ext0_veto)
|
|
76
|
+
changed |= self._change_xml(root, "EW", "Veto 1", self.ext1_veto)
|
|
77
|
+
changed |= self._change_xml(root, "EW", "Veto 2", self.ext2_veto)
|
|
78
|
+
changed |= self._change_xml(root, "EW", "Veto 3", self.ext3_veto)
|
|
79
|
+
return changed
|
|
80
|
+
|
|
81
|
+
def change_tcb_calculation_method(self, root):
|
|
82
|
+
changed = self._change_xml(root, "U16", "Calculation Method", self.tcb_calculation_method)
|
|
83
|
+
return changed
|
|
84
|
+
|
|
85
|
+
def change_tcb_settings(self, root):
|
|
86
|
+
changed = self._change_xml(root, "String", "Time Channel File", self.tcb_file)
|
|
87
|
+
changed |= self.change_tcb_calculation_method(root)
|
|
88
|
+
changed |= self._change_tcb_table(root)
|
|
89
|
+
return changed
|
|
90
|
+
|
|
91
|
+
def _change_tcb_table(self, root):
|
|
92
|
+
changed = False
|
|
93
|
+
for row in self.tcb_tables:
|
|
94
|
+
regime = str(row[0])
|
|
95
|
+
trange = str(row[1])
|
|
96
|
+
changed |= self._change_xml(root, "DBL", "TR%s From %s" % (regime, trange), row[2])
|
|
97
|
+
changed |= self._change_xml(root, "DBL", "TR%s To %s" % (regime, trange), row[3])
|
|
98
|
+
changed |= self._change_xml(root, "DBL", "TR%s Steps %s" % (regime, trange), row[4])
|
|
99
|
+
changed |= self._change_xml(root, "U16", "TR%s In Mode %s" % (regime, trange), row[5])
|
|
100
|
+
|
|
101
|
+
changed |= self.change_tcb_calculation_method(root)
|
|
102
|
+
return changed
|
|
103
|
+
|
|
104
|
+
def change_period_settings(self, root):
|
|
105
|
+
changed = self._change_xml(root, "EW", "Period Type", self.periods_type)
|
|
106
|
+
changed |= self._change_xml(
|
|
107
|
+
root, "I32", "Number Of Software Periods", self.periods_soft_num
|
|
108
|
+
)
|
|
109
|
+
changed |= self._change_xml(root, "EW", "Period Setup Source", self.periods_src)
|
|
110
|
+
changed |= self._change_xml(root, "DBL", "Hardware Period Sequences", self.periods_seq)
|
|
111
|
+
changed |= self._change_xml(root, "DBL", "Output Delay (us)", self.periods_delay)
|
|
112
|
+
changed |= self._change_xml(root, "String", "Period File", self.periods_file)
|
|
113
|
+
changed |= self._change_period_table(root)
|
|
114
|
+
return changed
|
|
115
|
+
|
|
116
|
+
def _change_period_table(self, root):
|
|
117
|
+
changed = False
|
|
118
|
+
for row in self.periods_settings:
|
|
119
|
+
period = row[0]
|
|
120
|
+
ptype = row[1]
|
|
121
|
+
frames = row[2]
|
|
122
|
+
output = row[3]
|
|
123
|
+
label = row[4]
|
|
124
|
+
changed |= self._change_xml(root, "EW", "Type %s" % period, ptype)
|
|
125
|
+
changed |= self._change_xml(root, "I32", "Frames %s" % period, frames)
|
|
126
|
+
changed |= self._change_xml(root, "U16", "Output %s" % period, output)
|
|
127
|
+
changed |= self._change_xml(root, "String", "Label %s" % period, label)
|
|
128
|
+
return changed
|
|
129
|
+
|
|
130
|
+
def _change_xml(self, xml, node, name, value):
|
|
131
|
+
"""
|
|
132
|
+
Helper func to change the xml.
|
|
133
|
+
Will not be set if the input is None.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
xml: The root of the xml
|
|
137
|
+
node: The node type
|
|
138
|
+
name: The name of the node
|
|
139
|
+
value: The new value to set
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
bool: True if the xml has been changed
|
|
143
|
+
"""
|
|
144
|
+
if value is not None:
|
|
145
|
+
for top in xml.iter(node):
|
|
146
|
+
n = top.find("Name")
|
|
147
|
+
if n.text == name:
|
|
148
|
+
v = top.find("Val")
|
|
149
|
+
v.text = str(value)
|
|
150
|
+
return True
|
|
151
|
+
return False
|