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,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Genie Database Access module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from genie_python.mysql_abstraction_layer import SQLAbstraction
|
|
8
|
+
|
|
9
|
+
SELECT_FOR_EXP_DETAILS = """
|
|
10
|
+
SELECT e.experimentID, u.name as userName, r.name as roleName,"""
|
|
11
|
+
""" t.startDate, e.duration FROM `experimentteams` t
|
|
12
|
+
JOIN experiment e ON e.experimentID = t.experimentID
|
|
13
|
+
JOIN user u ON u.userID = t.userID
|
|
14
|
+
JOIN role r ON r.roleID = t.roleID
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GetExperimentData:
|
|
19
|
+
"""
|
|
20
|
+
Class for storing the get_exp_data RB lookup command and related utility methods.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, instrument: str) -> None:
|
|
24
|
+
self.instrument = instrument
|
|
25
|
+
self._sql = SQLAbstraction(
|
|
26
|
+
dbid="exp_data", user="report", password="$report", host=self.instrument
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def get_sql(self) -> SQLAbstraction:
|
|
30
|
+
"""
|
|
31
|
+
Get the one and only SQL abstraction layer.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The SQL abstraction layer
|
|
35
|
+
"""
|
|
36
|
+
return self._sql
|
|
37
|
+
|
|
38
|
+
def _parameter_is_valid(self, value: bool | int | float | str, column: str, table: str) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Checks whether the given value exists in the table.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
value: the value to search for
|
|
44
|
+
column: the column to search in
|
|
45
|
+
table: the table to search in
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if value was found, False otherwise
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
result = self.get_sql().query(
|
|
52
|
+
command=f"SELECT * FROM `{table}` WHERE {column} = %s", bound_variables=(value,)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return True if result else False
|
|
56
|
+
|
|
57
|
+
class _GetExpDataReturn(TypedDict):
|
|
58
|
+
rb_number: int | str
|
|
59
|
+
user: str
|
|
60
|
+
role: str
|
|
61
|
+
start_date: str
|
|
62
|
+
duration: float
|
|
63
|
+
|
|
64
|
+
def get_exp_data(
|
|
65
|
+
self, rb: int | str = "%", user: str = "%", role: str = "%", verbose: bool = False
|
|
66
|
+
) -> list[_GetExpDataReturn]:
|
|
67
|
+
"""
|
|
68
|
+
Returns the data of experiments that match the given criteria,
|
|
69
|
+
or all if none is given, from the exp_data
|
|
70
|
+
database. If verbose is enabled, only pretty-print the data.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
rb (int, optional): The RB number of the experiment to look for, Defaults to Any.
|
|
74
|
+
user (str, optional): The name of the user who is running/has
|
|
75
|
+
run the experiment, Defaults to Any.
|
|
76
|
+
role (str, optional): The user role, Defaults to Any.
|
|
77
|
+
verbose (bool, optional): Pretty-print the data, Defaults to False.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
exp_data (list): The experiment(s) data as a list of dicts.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
NotFoundError: Thrown if a parameter's value was not found in the database.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
self._validate_parameters(rb, user, role)
|
|
87
|
+
|
|
88
|
+
args, sql = self._create_sql_statement_and_args(rb, role, user)
|
|
89
|
+
|
|
90
|
+
exp_data = self._query_database_for_data(args, sql)
|
|
91
|
+
|
|
92
|
+
if verbose:
|
|
93
|
+
if exp_data:
|
|
94
|
+
self._pretty_print(exp_data)
|
|
95
|
+
else:
|
|
96
|
+
raise NotFoundError(
|
|
97
|
+
f'Found no experiments that match the given criteria '
|
|
98
|
+
f'(RB: {rb if rb != "%" else "Any"}, '
|
|
99
|
+
f'User: {user if user != "%" else "Any"}, '
|
|
100
|
+
f'Role: {role if role != "%" else "Any"}).\n'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return exp_data
|
|
104
|
+
|
|
105
|
+
def _validate_parameters(self, rb: int | str, user: str, role: str) -> None:
|
|
106
|
+
if rb != "%" and not self._parameter_is_valid(rb, "experimentID", "experimentteams"):
|
|
107
|
+
raise NotFoundError(f"Found no experiments with RB number {rb}.")
|
|
108
|
+
if user != "%" and not self._parameter_is_valid(user, "name", "user"):
|
|
109
|
+
raise NotFoundError(
|
|
110
|
+
f'User with name "{user}" was not found. Please make sure the title and name are '
|
|
111
|
+
f'correct (e.g. "Dr John Smith").'
|
|
112
|
+
)
|
|
113
|
+
if role != "%" and not self._parameter_is_valid(role, "name", "role"):
|
|
114
|
+
roles = ", ".join(
|
|
115
|
+
[x[0] for x in self.get_sql().query_returning_cursor("SELECT name FROM `role`", ())]
|
|
116
|
+
)
|
|
117
|
+
raise NotFoundError(f'Role "{role}" was not found. Existing roles: {roles}')
|
|
118
|
+
|
|
119
|
+
def _query_database_for_data(
|
|
120
|
+
self, args: bool | int | float | str, sql: str
|
|
121
|
+
) -> list[_GetExpDataReturn]:
|
|
122
|
+
exp_data = []
|
|
123
|
+
for exp_id, user, role, start_date, duration in self.get_sql().query_returning_cursor(
|
|
124
|
+
sql, args
|
|
125
|
+
):
|
|
126
|
+
start_date = start_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
127
|
+
experiment_details = {
|
|
128
|
+
"rb_number": exp_id,
|
|
129
|
+
"user": user,
|
|
130
|
+
"role": role,
|
|
131
|
+
"start_date": start_date,
|
|
132
|
+
"duration": duration,
|
|
133
|
+
}
|
|
134
|
+
exp_data.append(experiment_details)
|
|
135
|
+
return exp_data
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _create_sql_statement_and_args(
|
|
139
|
+
rb: int | str, user: str, role: str
|
|
140
|
+
) -> tuple[bool | int | float | str, str]:
|
|
141
|
+
"""
|
|
142
|
+
Prepare the statement and bound variables
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
rb: rb number of the experiment
|
|
146
|
+
role: role being searched for
|
|
147
|
+
user: user being searched for
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
args: the bound variables
|
|
151
|
+
sql: the SQL command
|
|
152
|
+
"""
|
|
153
|
+
sql = SELECT_FOR_EXP_DETAILS + " WHERE "
|
|
154
|
+
where_clauses = [
|
|
155
|
+
"e.experimentID = %s" if rb != "%" else "e.experimentID LIKE '%'",
|
|
156
|
+
"upper(u.name) = upper(%s)" if user != "%" else "u.name LIKE '%'",
|
|
157
|
+
"upper(r.name) = upper(%s)" if role != "%" else "r.name LIKE '%'",
|
|
158
|
+
]
|
|
159
|
+
args = [x for x in [rb, user, role] if x != "%"]
|
|
160
|
+
sql += " AND ".join(where_clauses) + " ORDER BY experimentID DESC"
|
|
161
|
+
return args, sql
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _pretty_print(exp_data: list[_GetExpDataReturn]) -> None:
|
|
165
|
+
# For pretty printing
|
|
166
|
+
rb_padding = max(len(x) for x in [y["rb_number"] for y in exp_data])
|
|
167
|
+
user_padding = max(len(x) for x in [y["user"] for y in exp_data])
|
|
168
|
+
role_padding = max(len(x) for x in [y["role"] for y in exp_data])
|
|
169
|
+
|
|
170
|
+
for exp in exp_data:
|
|
171
|
+
print(
|
|
172
|
+
f'Experiment RB number: {exp["rb_number"]:{rb_padding}} | '
|
|
173
|
+
f'User: {exp["user"]:{user_padding}} | '
|
|
174
|
+
f'Role: {exp["role"]:{role_padding}} | '
|
|
175
|
+
f'Start date: {exp["start_date"]} | '
|
|
176
|
+
f'Duration: {exp["duration"]}'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class NotFoundError(IOError):
|
|
181
|
+
"""
|
|
182
|
+
Exception that is thrown if a value was not found in the database.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(self, message: str) -> None:
|
|
186
|
+
super(NotFoundError, self).__init__(message)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Logging module for genie"""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import logging
|
|
5
|
+
import logging.config
|
|
6
|
+
|
|
7
|
+
import graypy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def send(self, s):
|
|
11
|
+
"""
|
|
12
|
+
Needed otherwise graypy spits out logs if the DNS alias cannot be resolved.
|
|
13
|
+
See https://github.com/ISISComputingGroup/IBEX/issues/7059
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
if len(s) < self.gelf_chunker.chunk_size:
|
|
17
|
+
super(graypy.GELFUDPHandler, self).send(s)
|
|
18
|
+
else:
|
|
19
|
+
for chunk in self.gelf_chunker.chunk_message(s):
|
|
20
|
+
super(graypy.GELFUDPHandler, self).send(chunk)
|
|
21
|
+
except Exception:
|
|
22
|
+
# logging to graypy failed, silently fail rather than throwing
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
graypy.GELFUDPHandler.send = send
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
import socket
|
|
30
|
+
from contextlib import contextmanager
|
|
31
|
+
from time import localtime, strftime
|
|
32
|
+
|
|
33
|
+
from genie_python.version import VERSION
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InstrumentFilter(logging.Filter):
|
|
37
|
+
"""For Graylog fields (https://graypy.readthedocs.io/en/latest/readme.html#adding-custom-logging-fields)"""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self.instrument = ""
|
|
41
|
+
self.sim_mode = False
|
|
42
|
+
self.genie_version = VERSION
|
|
43
|
+
self.reset()
|
|
44
|
+
|
|
45
|
+
def set_inst_name(self, inst_name):
|
|
46
|
+
self.instrument = inst_name
|
|
47
|
+
|
|
48
|
+
def set_sim_mode(self, sim_mode):
|
|
49
|
+
self.sim_mode = sim_mode
|
|
50
|
+
|
|
51
|
+
def filter(self, record):
|
|
52
|
+
record.instrument = self.instrument
|
|
53
|
+
record.command_called = self.command_called
|
|
54
|
+
record.function_args = self.function_args
|
|
55
|
+
record.function_kwargs = self.function_kwargs
|
|
56
|
+
record.time_taken = self.time_taken
|
|
57
|
+
record.exception_text = self.exception_text
|
|
58
|
+
record.from_channel_access = str(self.from_channel_access) # need to cast bool to str
|
|
59
|
+
record.from_ibex = str(os.getenv("FROM_IBEX"))
|
|
60
|
+
record.genie_version = self.genie_version
|
|
61
|
+
record.is_sim_mode = str(self.sim_mode)
|
|
62
|
+
self.reset()
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
def reset(self):
|
|
66
|
+
"""
|
|
67
|
+
Reset filter fields back to blank values.
|
|
68
|
+
"""
|
|
69
|
+
self.command_called = ""
|
|
70
|
+
self.function_args = ""
|
|
71
|
+
self.function_kwargs = ""
|
|
72
|
+
self.time_taken = 0
|
|
73
|
+
self.exception_text = ""
|
|
74
|
+
self.from_channel_access = False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
filter = InstrumentFilter()
|
|
78
|
+
|
|
79
|
+
vhd_build_machines = ["NDHSPARE11"]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class LoggingConfigurer:
|
|
83
|
+
@staticmethod
|
|
84
|
+
def get_file_name():
|
|
85
|
+
curr_time = localtime()
|
|
86
|
+
current_date_time = strftime("%Y-%m-%d-%a", curr_time)
|
|
87
|
+
return f"genie-{current_date_time}.log"
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def get_log_file_dir():
|
|
91
|
+
if socket.gethostname() in vhd_build_machines:
|
|
92
|
+
return os.path.join("C:", os.sep, "genie_python_logs")
|
|
93
|
+
else:
|
|
94
|
+
if os.name == "nt":
|
|
95
|
+
return os.path.join("C:", os.sep, "Instrument", "Var", "logs", "genie_python")
|
|
96
|
+
else:
|
|
97
|
+
return os.path.join("/tmp/{}/genie_python".format(getpass.getuser()))
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def get_log_file_path():
|
|
101
|
+
logs_dir = LoggingConfigurer.get_log_file_dir()
|
|
102
|
+
filename = LoggingConfigurer.get_file_name()
|
|
103
|
+
return os.path.join(logs_dir, filename)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def get_logging_config():
|
|
107
|
+
return {
|
|
108
|
+
"version": 1,
|
|
109
|
+
"disable_existing_loggers": False,
|
|
110
|
+
"formatters": {
|
|
111
|
+
"verbose": {
|
|
112
|
+
"format": "[{asctime}] [{process:d}:{thread:d}] [{levelname}]\t{message}",
|
|
113
|
+
"style": "{",
|
|
114
|
+
"datefmt": "%Y-%m-%dT%H:%M:%S",
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"handlers": {
|
|
118
|
+
"graypy": {
|
|
119
|
+
"level": "DEBUG",
|
|
120
|
+
"class": "graypy.GELFUDPHandler",
|
|
121
|
+
"host": "ino.isis.cclrc.ac.uk",
|
|
122
|
+
"port": 12201,
|
|
123
|
+
"formatter": "verbose",
|
|
124
|
+
},
|
|
125
|
+
"file": {
|
|
126
|
+
"level": "DEBUG",
|
|
127
|
+
"formatter": "verbose",
|
|
128
|
+
"class": "logging.FileHandler",
|
|
129
|
+
"filename": LoggingConfigurer.get_log_file_path(),
|
|
130
|
+
"mode": "a",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
"loggers": {
|
|
134
|
+
"genie_python_graylogger": {
|
|
135
|
+
"handlers": ["graypy", "file"],
|
|
136
|
+
"level": "DEBUG",
|
|
137
|
+
"propagate": False,
|
|
138
|
+
"namer": LoggingConfigurer.get_file_name(),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@contextmanager
|
|
145
|
+
def genie_logger():
|
|
146
|
+
try:
|
|
147
|
+
logging.config.dictConfig(LoggingConfigurer.get_logging_config())
|
|
148
|
+
yield logging.getLogger("genie_python_graylogger")
|
|
149
|
+
finally:
|
|
150
|
+
logging.shutdown()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class GenieLogger:
|
|
154
|
+
def __init__(self, sim_mode=False):
|
|
155
|
+
self.sim_mode = sim_mode
|
|
156
|
+
self.logs_dir = LoggingConfigurer.get_log_file_dir()
|
|
157
|
+
if not os.path.exists(self.logs_dir):
|
|
158
|
+
os.makedirs(self.logs_dir)
|
|
159
|
+
|
|
160
|
+
filter.set_sim_mode(self.sim_mode)
|
|
161
|
+
with genie_logger() as logger:
|
|
162
|
+
logger.addFilter(filter)
|
|
163
|
+
|
|
164
|
+
def log_info_msg(self, message):
|
|
165
|
+
with genie_logger() as logger:
|
|
166
|
+
logger.info(self._get_message_with_mode(message))
|
|
167
|
+
|
|
168
|
+
def log_command(self, function_name, arguments, command_exception, time_taken=None):
|
|
169
|
+
filter.command_called = function_name
|
|
170
|
+
filter.function_args = arguments
|
|
171
|
+
filter.exception_text = command_exception
|
|
172
|
+
if time_taken is not None:
|
|
173
|
+
filter.time_taken = time_taken
|
|
174
|
+
with genie_logger() as logger:
|
|
175
|
+
logger.debug(self._get_message_with_mode(f"{function_name} {arguments}"))
|
|
176
|
+
|
|
177
|
+
def log_command_error_msg(self, function_name, error_msg):
|
|
178
|
+
error_msg = f"An exception has occurred as a result of the command:\n{error_msg}"
|
|
179
|
+
filter.command_called = function_name
|
|
180
|
+
filter.exception_text = error_msg
|
|
181
|
+
with genie_logger() as logger:
|
|
182
|
+
logger.error(self._get_message_with_mode(error_msg))
|
|
183
|
+
|
|
184
|
+
def log_error_msg(self, error_msg):
|
|
185
|
+
filter.exception_text = error_msg
|
|
186
|
+
with genie_logger() as logger:
|
|
187
|
+
logger.error(self._get_message_with_mode(error_msg))
|
|
188
|
+
|
|
189
|
+
def log_ca_msg(self, error_msg):
|
|
190
|
+
"""Log the CA error (from CaChannel)"""
|
|
191
|
+
filter.exception_text = error_msg
|
|
192
|
+
with genie_logger() as logger:
|
|
193
|
+
logger.error(self._get_message_with_mode(error_msg))
|
|
194
|
+
|
|
195
|
+
def set_sim_mode(self, sim_mode):
|
|
196
|
+
self.sim_mode = sim_mode
|
|
197
|
+
filter.set_sim_mode(sim_mode)
|
|
198
|
+
|
|
199
|
+
def _get_message_with_mode(self, msg):
|
|
200
|
+
return f"(SIMULATION) {msg}" if self.sim_mode is True else msg
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wrapping of p4p in genie_python
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import absolute_import, print_function
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
from builtins import object
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import TYPE_CHECKING, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from p4p import Value
|
|
13
|
+
from p4p.client.thread import Context, Subscription
|
|
14
|
+
|
|
15
|
+
from .channel_access_exceptions import WriteAccessException
|
|
16
|
+
from .utilities import waveform_to_string
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from genie_python.genie import PVValue
|
|
20
|
+
|
|
21
|
+
TIMEOUT = 15 # Default timeout for PV set/get
|
|
22
|
+
EXIST_TIMEOUT = 3 # Separate smaller timeout for pv_exists() and searchw() operations
|
|
23
|
+
CACHE = threading.local()
|
|
24
|
+
CACHE_LOCK = threading.local()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class P4PWrapper(object):
|
|
28
|
+
context: Optional[Context] = None
|
|
29
|
+
error_log_function: Optional[Callable[[str], None]] = None
|
|
30
|
+
|
|
31
|
+
# noinspection PyPep8Naming
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _log_error(message: str) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Log an error
|
|
36
|
+
Args:
|
|
37
|
+
message: message to log
|
|
38
|
+
"""
|
|
39
|
+
if P4PWrapper.error_log_function is not None:
|
|
40
|
+
try:
|
|
41
|
+
P4PWrapper.error_log_function(message)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
else:
|
|
45
|
+
print("PVERROR: {}".format(message))
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def set_pv_value(
|
|
49
|
+
name: str,
|
|
50
|
+
value: "PVValue",
|
|
51
|
+
wait: bool = False,
|
|
52
|
+
timeout: float = TIMEOUT,
|
|
53
|
+
safe_not_quick: bool = True,
|
|
54
|
+
) -> None:
|
|
55
|
+
if safe_not_quick:
|
|
56
|
+
P4PWrapper._check_for_disp(name)
|
|
57
|
+
context = P4PWrapper.get_context()
|
|
58
|
+
context.put(name, value, timeout=timeout, wait=wait)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def clear_monitor(name: str, timeout: float) -> None:
|
|
62
|
+
try:
|
|
63
|
+
lock = CACHE_LOCK.lock
|
|
64
|
+
except AttributeError:
|
|
65
|
+
lock = CACHE_LOCK.lock = threading.RLock()
|
|
66
|
+
with lock:
|
|
67
|
+
try:
|
|
68
|
+
subscriptions = CACHE.subscriptions
|
|
69
|
+
if name in subscriptions:
|
|
70
|
+
subscriptions.pop(name).close()
|
|
71
|
+
except AttributeError:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def get_pv_value(
|
|
76
|
+
name: str,
|
|
77
|
+
to_string: bool = False,
|
|
78
|
+
timeout: float = TIMEOUT,
|
|
79
|
+
use_numpy: Optional[bool] = None,
|
|
80
|
+
) -> "PVValue":
|
|
81
|
+
context = P4PWrapper.get_context()
|
|
82
|
+
output = context.get(name, timeout=timeout)
|
|
83
|
+
if isinstance(output, Exception):
|
|
84
|
+
raise output
|
|
85
|
+
|
|
86
|
+
# Required to convince pyright the type won't be [Exception] which is only a valid response
|
|
87
|
+
# to a list of names. This should be replaced by proper handling for [Value] and [Exception]
|
|
88
|
+
# In a non-minimal/equivalent to CaChannel implementation.
|
|
89
|
+
assert isinstance(output, Value)
|
|
90
|
+
|
|
91
|
+
val = output.value
|
|
92
|
+
|
|
93
|
+
# If it's still a Value type then it's an Enum, so get the index or choice.
|
|
94
|
+
if isinstance(val, Value):
|
|
95
|
+
return val.choices[val.index]
|
|
96
|
+
|
|
97
|
+
if to_string:
|
|
98
|
+
val = str(val)
|
|
99
|
+
return val
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def get_pv_timestamp(name: str, timeout: float = TIMEOUT) -> Tuple[int, int]:
|
|
103
|
+
context = P4PWrapper.get_context()
|
|
104
|
+
output = context.get(name, timeout=timeout)
|
|
105
|
+
if isinstance(output, Exception):
|
|
106
|
+
raise output
|
|
107
|
+
|
|
108
|
+
# Required to convince pyright the type won't be [Exception] which is only a valid response
|
|
109
|
+
# to a list of names. This should be replaced by proper handling for [Value] and [Exception]
|
|
110
|
+
# In a non-minimal/equivalent to CaChannel implementation.
|
|
111
|
+
assert isinstance(output, Value)
|
|
112
|
+
|
|
113
|
+
time = output.timeStamp
|
|
114
|
+
return time.get("secondsPastEpoch"), time.get("nanoseconds")
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def pv_exists(name: str, timeout: float) -> bool:
|
|
118
|
+
try:
|
|
119
|
+
P4PWrapper.get_pv_value(name, timeout=timeout)
|
|
120
|
+
return True
|
|
121
|
+
except TimeoutError:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def add_monitor(
|
|
126
|
+
name: str,
|
|
127
|
+
call_back_function: "Callable[[PVValue, str, str], None]",
|
|
128
|
+
link_alarm_on_disconnect: bool = True,
|
|
129
|
+
to_string: bool = False,
|
|
130
|
+
use_numpy: Optional[bool] = None,
|
|
131
|
+
) -> Subscription:
|
|
132
|
+
def _process_call_back(response: Value | Exception) -> None:
|
|
133
|
+
if isinstance(response, Exception):
|
|
134
|
+
P4PWrapper._log_error(str(response))
|
|
135
|
+
return
|
|
136
|
+
value = response.get("value")
|
|
137
|
+
if isinstance(value, Value):
|
|
138
|
+
value = value.choices[value.index]
|
|
139
|
+
if to_string:
|
|
140
|
+
# Could see if the element count is > 1 instead
|
|
141
|
+
if isinstance(value, list):
|
|
142
|
+
value = waveform_to_string(value)
|
|
143
|
+
else:
|
|
144
|
+
value = str(value)
|
|
145
|
+
elif isinstance(value, Value):
|
|
146
|
+
value = value.index
|
|
147
|
+
|
|
148
|
+
call_back_function(
|
|
149
|
+
value,
|
|
150
|
+
response.get("alarm").get("severity"),
|
|
151
|
+
response.get("alarm").get("status"),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
context = P4PWrapper.get_context()
|
|
155
|
+
subscription = context.monitor(name, _process_call_back, notify_disconnect=True)
|
|
156
|
+
|
|
157
|
+
# Add to a dict of subscriptions to reproduce ability to close a monitor by its name.
|
|
158
|
+
try:
|
|
159
|
+
lock = CACHE_LOCK.lock
|
|
160
|
+
except AttributeError:
|
|
161
|
+
lock = CACHE_LOCK.lock = threading.RLock()
|
|
162
|
+
with lock:
|
|
163
|
+
try:
|
|
164
|
+
subscriptions = CACHE.subscriptions
|
|
165
|
+
subscriptions.update({name: subscription})
|
|
166
|
+
except AttributeError:
|
|
167
|
+
CACHE.subscriptions = {name: Subscription}
|
|
168
|
+
return subscription
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def get_context(autowrap: bool = False) -> Context:
|
|
172
|
+
try:
|
|
173
|
+
lock = CACHE_LOCK.lock
|
|
174
|
+
except AttributeError:
|
|
175
|
+
lock = CACHE_LOCK.lock = threading.RLock()
|
|
176
|
+
|
|
177
|
+
with lock:
|
|
178
|
+
try:
|
|
179
|
+
thread_context = CACHE.context
|
|
180
|
+
except AttributeError:
|
|
181
|
+
thread_context = CACHE.context = Context("pva", nt=autowrap)
|
|
182
|
+
if thread_context is None:
|
|
183
|
+
thread_context = Context("pva", nt=autowrap)
|
|
184
|
+
return thread_context
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _check_for_disp(name: str) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Check if DISP is set on a PV. If passed a field instead of a PV, do nothing.
|
|
190
|
+
Only check DISP if it exists.
|
|
191
|
+
"""
|
|
192
|
+
if (
|
|
193
|
+
".DISP" not in name
|
|
194
|
+
): # Do not check for DISP if it's already in the name of the PV to check
|
|
195
|
+
if "." in name: # If given a field on a PV, check the PV itself if DISP is set
|
|
196
|
+
name = name.split(".")[0]
|
|
197
|
+
_disp_name = "{}.DISP".format(name)
|
|
198
|
+
if P4PWrapper.pv_exists(_disp_name, 0) and P4PWrapper.get_pv_value(_disp_name) != "0":
|
|
199
|
+
raise WriteAccessException("{} (DISP is set)".format(name))
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def close_context() -> None:
|
|
203
|
+
P4PWrapper.get_context().close()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function
|
|
2
|
+
|
|
3
|
+
from builtins import object, str
|
|
4
|
+
from functools import wraps
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _plotting_func(func):
|
|
8
|
+
"""
|
|
9
|
+
Decorator for functions that interact with MPL.
|
|
10
|
+
|
|
11
|
+
Specifically, turns off interactive mode while the graphs are being manipulated and turns
|
|
12
|
+
it back on (and shows the graph) when finished.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@wraps(func)
|
|
16
|
+
def wrapper(*args, **kwargs):
|
|
17
|
+
import matplotlib.pyplot as pyplt
|
|
18
|
+
|
|
19
|
+
was_interactive = pyplt.isinteractive()
|
|
20
|
+
pyplt.ioff()
|
|
21
|
+
result = func(*args, **kwargs)
|
|
22
|
+
pyplt.interactive(was_interactive)
|
|
23
|
+
pyplt.show(block=False)
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
return wrapper
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SpectraPlot(object):
|
|
30
|
+
@_plotting_func
|
|
31
|
+
def __init__(self, api, spectrum, period, dist):
|
|
32
|
+
import matplotlib.pyplot as pyplt
|
|
33
|
+
|
|
34
|
+
self.api = api
|
|
35
|
+
self.spectra = []
|
|
36
|
+
self.fig = pyplt.figure()
|
|
37
|
+
self.ax = pyplt.subplot(111)
|
|
38
|
+
self.ax.autoscale_view(True, True, True)
|
|
39
|
+
self.ax.set_xlabel("Time")
|
|
40
|
+
self.ax.set_ylabel("Counts")
|
|
41
|
+
self.ax.set_title("Spectrum {}".format(spectrum))
|
|
42
|
+
self.add_spectrum(spectrum, period, dist)
|
|
43
|
+
|
|
44
|
+
@_plotting_func
|
|
45
|
+
def add_spectrum(self, spectrum, period=1, dist=True):
|
|
46
|
+
self.spectra.append((spectrum, period, dist))
|
|
47
|
+
data = self.api.dae.get_spectrum(spectrum, period, dist)
|
|
48
|
+
name = "Spect {}".format(spectrum)
|
|
49
|
+
self.ax.plot(data["time"], data["signal"], label=name)
|
|
50
|
+
self.__update_legend()
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
@_plotting_func
|
|
54
|
+
def refresh(self):
|
|
55
|
+
for i in range(len(self.ax.lines)):
|
|
56
|
+
data = self.api.dae.get_spectrum(
|
|
57
|
+
self.spectra[0][0], self.spectra[0][1], self.spectra[0][2]
|
|
58
|
+
)
|
|
59
|
+
line = self.ax.lines[i]
|
|
60
|
+
line.set_data(data["time"], data["signal"])
|
|
61
|
+
self.ax.autoscale_view(True, True, True)
|
|
62
|
+
|
|
63
|
+
@_plotting_func
|
|
64
|
+
def delete_plot(self, plotnum):
|
|
65
|
+
del self.ax.lines[plotnum]
|
|
66
|
+
del self.spectra[plotnum]
|
|
67
|
+
self.__update_legend()
|
|
68
|
+
|
|
69
|
+
def __update_legend(self):
|
|
70
|
+
handles, labels = self.ax.get_legend_handles_labels()
|
|
71
|
+
self.ax.legend(handles, labels)
|
|
72
|
+
|
|
73
|
+
def __repr__(self):
|
|
74
|
+
if len(self.spectra) > 0:
|
|
75
|
+
return "Spectra plot ({})".format(", ".join([str(x[0]) for x in self.spectra]))
|
|
76
|
+
else:
|
|
77
|
+
return "Spectra plot (empty)"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from builtins import object
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PrePostCmdManager(object):
|
|
5
|
+
"""
|
|
6
|
+
A class to manager the precmd and postcmd commands such as used in begin, end, abort, resume, pause.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.begin_precmd = lambda **pars: None
|
|
11
|
+
self.begin_postcmd = lambda **pars: None
|
|
12
|
+
self.abort_precmd = lambda **pars: None
|
|
13
|
+
self.abort_postcmd = lambda **pars: None
|
|
14
|
+
self.end_precmd = lambda **pars: None
|
|
15
|
+
self.end_postcmd = lambda **pars: None
|
|
16
|
+
self.pause_precmd = lambda **pars: None
|
|
17
|
+
self.pause_postcmd = lambda **pars: None
|
|
18
|
+
self.resume_precmd = lambda **pars: None
|
|
19
|
+
self.resume_postcmd = lambda **pars: None
|
|
20
|
+
self.cset_precmd = lambda **pars: True
|
|
21
|
+
self.cset_postcmd = lambda **pars: None
|