-
Notifications
You must be signed in to change notification settings - Fork 76
/
collect_ad_boilerplate.py
executable file
·279 lines (227 loc) · 9.26 KB
/
collect_ad_boilerplate.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
#!/usr/bin/env python3
import argparse
import logging
import os
import inflection
def write_detector_class(boilerplate_file, dev_name, det_name, cam_name):
"""
Writes boilerplate 'Detector' class for ophyd/areadetector/detectors.
This script automates the creation of ophyd classes for areaDetector
drivers by scraping their *.template files. It is called by developers as
needed.
Parameters
----------
boilerplate_file : io.TextIOWrapper
Open temporary file for writing boilerplate
dev_name : str
Name of device type/make. Ex. PICam, Eiger, etc.
det_name : str
Name of detector class, ex. PICamDetector, EigerDetector, etc.
cam_name : str
Name of cam class, ex. PICamDetectorCam, EigerDetectorCam, etc.
"""
boilerplate_file.write(
f"""
class {det_name}(DetectorBase):
_html_docs = ['{dev_name}Doc.html']
cam = C(cam.{cam_name}, 'cam1:')
"""
)
def parse_pv_structure(driver_dir):
"""
Reads all .template files in the specified driver directory and maps them
to the appropriate EPICS signal class in ophyd
Also determines if the Cam class should extend the FileBase class as well.
Parameters
----------
driver_dir : PathLike
Path to the areaDetector driver
Returns
-------
pv_to_signal_mapping : dict
Dict mapping PVs to the EPICS signal they fall under
include_file_base : bool
True if the NDFile.template file is included, otherwise False
"""
# Find the template directory following the standard areaDetector project format
template_dir = driver_dir
for dir in os.listdir(driver_dir):
if os.path.isdir(os.path.join(driver_dir, dir)) and dir.endswith("App"):
template_dir = os.path.join(template_dir, dir, "Db")
break
logging.debug(f"Found template dir: {template_dir}")
# Create a list of file paths to all template files.
template_files = []
for file in os.listdir(template_dir):
file_path = os.path.join(template_dir, file)
if os.path.isfile(file_path) and file.endswith(".template"):
template_files.append(file_path)
logging.debug(f"Found template file {file}")
# Dict mapping pv name to appropriate EPICS signal, based on typical
# PV and PV_RBV format for areaDetector
pv_to_signal_mapping = {}
include_file_base = False
for file in template_files:
logging.debug(f"Collecting pv info from {os.path.basename(file)}")
with open(file, "r") as fp:
lines = fp.readlines()
for line in lines:
# If NDFile.template is included, we need to extend FileBase as well.
if line.startswith('include "NDFile.template"'):
logging.debug(f"Driver AD{dev_name} uses the NDFile.template file.")
include_file_base = True
# identify any lines that start with 'record'
if line.startswith("record"):
# Get the name of the PV.
# Ex:
# record(stringin, "$(P)$(R)Description_RBV") Splits into
# ['record(stringin, "$(P', '$(R', 'Description_RBV"', '']
# The PV name is the 3rd element, so array index 2, and we remove the last character, '"'
pv_name = line.split(")")[2][:-1]
# Check if it is a readback PV
if pv_name.endswith("_RBV"):
pv_name_without_rbv = pv_name[: -len("_RBV")]
# If it has a partner PV, switch the signal to SignalWithRBV
if pv_name_without_rbv in pv_to_signal_mapping.keys():
logging.debug(
f"Identified {pv_name_without_rbv} as a record w/ RBV"
)
pv_to_signal_mapping[pv_name_without_rbv] = "SignalWithRBV"
# Otherwise, it is a read only PV, so use EpicsSignalRO
else:
logging.debug(f"Identified read-only record {pv_name}")
pv_to_signal_mapping[pv_name] = "EpicsSignalRO"
else:
# Otherwise, use the default EpicsSignal
logging.debug(f"Found record {pv_name}")
pv_to_signal_mapping[pv_name] = "EpicsSignal"
return pv_to_signal_mapping, include_file_base
def write_cam_class(
boilerplate_file,
pv_to_signal_mapping,
include_file_base,
dev_name,
det_name,
cam_name,
):
"""
Function that writes the boilerplate cam class. This includes the default configuration
attributes, along with all the attributes extracted from the template file.
This function uses the inflection library's `underscore` function to convert a PV name
into an attribute name
Examples:
EnableCallbacks -> enable_callbacks
EVTLoadGainFile -> evt_load_gain_file
Parameters
----------
boilerplate_file : io.TextIOWrapper
Open boilerplate file
pv_to_signal_mapping : dict
Dictionary mapping PVs to the EPICS signal they fall under
include_file_base : bool
If true, we extend the FileBase class, otherwise we don't
dev_name : str
Name of device type/make. Ex. PICam, Eiger, etc.
det_name : str
Name of detector class, ex. PICamDetector, EigerDetector, etc.
cam_name : str
Name of cam class, ex. PICamDetectorCam, EigerDetectorCam, etc.
"""
# Extend from FileBase class as well if necessary
file_base = ""
if include_file_base:
file_base = ", FileBase"
# Write class boilerplate
boilerplate_file.write(
f"""
class {cam_name}(CamBase{file_base}):
_html_docs = ['{dev_name}Doc.html']
_default_configuration_attrs = (
CamBase._default_configuration_attrs
)
"""
)
# Write attribute for each discovered PV, with appropriate EPICS signal class
for pv in pv_to_signal_mapping.keys():
pv_name = pv
if pv_name.endswith("_RBV"):
pv_name = pv[: -len("_RBV")]
# Generate attribute name from PV name. Uses inflection library
attribute_name = inflection.underscore(pv_name)
# attribute_name = re.sub('(?<!^)(?=[A-Z])', '_', pv_name).lower()
boilerplate_file.write(
f" {attribute_name} = ADCpt({pv_to_signal_mapping[pv]}, '{pv}')\n"
)
def parse_args():
"""
usage: collect_ad_boilerplate.py [-h] [-t TARGET] [-d]
Utility for creating boilerplate areaDetector ophyd classes
optional arguments:
-h, --help show this help message and exit
-t TARGET, --target TARGET
Location of locally installed areaDetector driver
folder structure.
-d, --debug Enable debug logging for the script.
"""
parser = argparse.ArgumentParser(
description="Utility for creating boilerplate areaDetector ophyd classes"
)
parser.add_argument(
"-t",
"--target",
help="Location of locally installed areaDetector driver folder structure.",
)
parser.add_argument(
"-d",
"--debug",
action="store_true",
help="Enable debug logging for the script.",
)
args = vars(parser.parse_args())
# Set logging level
log_level = logging.ERROR
if args["debug"]:
log_level = logging.DEBUG
if args["target"] is None:
return os.path.abspath("."), log_level
else:
return args["target"], log_level
if __name__ == "__main__":
# Check if input is valid
driver_dir, log_level = parse_args()
if not os.path.exists(driver_dir) or not os.path.isdir(driver_dir):
logging.error(f"Input {driver_dir} does not exist or is not a directory!")
exit(1)
logging.basicConfig(level=log_level)
# Check if specified target is an areaDetector driver
driver_name = os.path.basename(driver_dir)
if not driver_name.startswith("AD"):
logging.error(
f"Specified driver directory {driver_name} could not be identified as an areaDetector driver!"
)
exit(1)
# Collect device, detector, and cam names
dev_name = driver_name[2:]
det_name = f"{dev_name}Detector"
cam_name = f"{det_name}Cam"
logging.debug(
f"Creating boilerplate for {dev_name}, with classes {det_name} and {cam_name}"
)
# Create boilerplate file with .py extension for syntax highlighting
boilerplate_file_name = f"{det_name}_boilerplate.py"
# Create boilerplate temp file
with open(boilerplate_file_name, "w") as boilerplate_file:
# Create the detector class for ophyd/areadetector/detectors
write_detector_class(boilerplate_file, dev_name, det_name, cam_name)
# Collect PV information from detector driver
driver_template, include_file_base = parse_pv_structure(driver_dir)
# Create boilerplate cam class for ophyd/areadetector/cam
write_cam_class(
boilerplate_file,
driver_template,
include_file_base,
dev_name,
det_name,
cam_name,
)
print(f"Done. Temporary boilerplate file saved to {boilerplate_file_name}")