1 | #!/usr/bin/env python |
---|
2 | |
---|
3 | ''' |
---|
4 | Read an EPICS .db database file into memory. |
---|
5 | ''' |
---|
6 | |
---|
7 | |
---|
8 | ########### SVN repository information ################### |
---|
9 | # $Date: 2011-07-14 16:18:18 +0000 (Thu, 14 Jul 2011) $ |
---|
10 | # $Author: jemian $ |
---|
11 | # $Revision: 541 $ |
---|
12 | # $URL: topdoc/src/TopDoc/EpicsDatabase.py $ |
---|
13 | # $Id: EpicsDatabase.py 541 2011-07-14 16:18:18Z jemian $ |
---|
14 | ########### SVN repository information ################### |
---|
15 | |
---|
16 | |
---|
17 | import os |
---|
18 | import pprint |
---|
19 | import utilities |
---|
20 | import TokenLog |
---|
21 | |
---|
22 | |
---|
23 | class Db(): |
---|
24 | ''' |
---|
25 | Read an EPICS .db database file into memory. |
---|
26 | The internal representation of the file is in self.pvDict. |
---|
27 | This is a Python dictionary of dictionaries. Each outer dictionary |
---|
28 | item is for a specific instance of an EPICS process variable (PV). |
---|
29 | The internal dictionary keys are field names and the dictionary values |
---|
30 | are the field values before macro expansion. |
---|
31 | |
---|
32 | Important methods provided are: |
---|
33 | |
---|
34 | ================== ==================== |
---|
35 | method description |
---|
36 | ================== ==================== |
---|
37 | :func:`countPVs` number of PVs that have been defined |
---|
38 | :func:`getPVs` Get a list of the EPICS PVs defined with optional macro substitutions. |
---|
39 | :func:`getFull` Get the full specification of the PVs and fields with optional macro substitutions. |
---|
40 | :func:`keyName` Find the dictionary key that expands to pvName. |
---|
41 | :func:`xrefDict` Cross-references the 'NAME' field between defined and macro-expanded values. |
---|
42 | ================== ==================== |
---|
43 | |
---|
44 | From the EPICS Application Developer's Guide |
---|
45 | |
---|
46 | :see: http://www.aps.anl.gov/epics/base/R3-14/12-docs/AppDevGuide/node7.html |
---|
47 | |
---|
48 | 6.3.2 Unquoted Strings |
---|
49 | |
---|
50 | In the summary section, some values are shown as quoted strings and some unquoted. |
---|
51 | The actual rule is that any string consisting of only the following characters |
---|
52 | does not have to be quoted unless it contains one of the above keywords: |
---|
53 | |
---|
54 | :: |
---|
55 | |
---|
56 | a-z A-Z 0-9 _ -- : . [ ] < > ; |
---|
57 | |
---|
58 | :: |
---|
59 | |
---|
60 | my regexp: [a-zA-Z0-9_-:.\[\]<>;] (What about these? -- [ ]) |
---|
61 | |
---|
62 | These are also the legal characters for process variable names. |
---|
63 | Thus in many cases quotes are not needed. |
---|
64 | |
---|
65 | 6.3.3 Quoted Strings |
---|
66 | |
---|
67 | A quoted string can contain any ascii character except the quote character ". |
---|
68 | The quote character itself can given by using \ as an escape. |
---|
69 | For example "\"" is a quoted string containing the single character ". |
---|
70 | |
---|
71 | 6.3.4 Macro Substitution |
---|
72 | |
---|
73 | Macro substitutions are permitted inside quoted strings. |
---|
74 | Macro instances take the form: |
---|
75 | |
---|
76 | :: |
---|
77 | |
---|
78 | $(name) |
---|
79 | |
---|
80 | or |
---|
81 | |
---|
82 | :: |
---|
83 | |
---|
84 | ${name} |
---|
85 | |
---|
86 | There is no distinction between the use of parentheses or braces |
---|
87 | for delimiters, although the two must match for a given macro instance. |
---|
88 | The macro name can be made up from other macros, for example: |
---|
89 | |
---|
90 | :: |
---|
91 | |
---|
92 | $(name_$(sel)) |
---|
93 | |
---|
94 | A macro instance can also provide a default value that is used when no |
---|
95 | macro with the given name is defined. The default value can be defined |
---|
96 | in terms of other macros if desired, but cannot contain any unescaped |
---|
97 | comma characters. The syntax for specifying a default value is as follows: |
---|
98 | |
---|
99 | :: |
---|
100 | |
---|
101 | $(name=default) |
---|
102 | |
---|
103 | Finally macro instances can also contain definitions of other macros, |
---|
104 | which can (temporarily) override any existing values for those macros |
---|
105 | but are in scope only for the duration of the expansion of this macro |
---|
106 | instance. These definitions consist of name=value sequences separated |
---|
107 | by commas, for example: |
---|
108 | |
---|
109 | :: |
---|
110 | |
---|
111 | $(abcd=$(a)$(b)$(c)$(d),a=A,b=B,c=C,d=D) |
---|
112 | |
---|
113 | ... |
---|
114 | |
---|
115 | 6.14 record - Record Instance |
---|
116 | |
---|
117 | 6.14.1 Format |
---|
118 | |
---|
119 | :: |
---|
120 | |
---|
121 | record(record_type, record_name) { |
---|
122 | include "filename" |
---|
123 | alias(alias_name) |
---|
124 | field(field_name, "field_value") |
---|
125 | info(info_name, "info_value") |
---|
126 | ... |
---|
127 | } |
---|
128 | alias(record_name, alias_name) |
---|
129 | |
---|
130 | MACRO DEFINITION |
---|
131 | |
---|
132 | :: |
---|
133 | |
---|
134 | $(name) typical |
---|
135 | ${name} alternate |
---|
136 | $(name_$(sel)) embedded |
---|
137 | $(name=default) default value if not defined |
---|
138 | $(abcd=$(a)$(b)$(c)$(d),a=A,b=B,c=C,d=D) define other macros |
---|
139 | ''' |
---|
140 | |
---|
141 | def __init__(self, dbFilename): |
---|
142 | ''' |
---|
143 | Constructor. Try to read and interpret the named file. |
---|
144 | |
---|
145 | :param dbFilename: string with name of .db file to open |
---|
146 | ''' |
---|
147 | self.filename = None |
---|
148 | self.absolute_filename = None |
---|
149 | self.buf = None |
---|
150 | self.pvDict = {} |
---|
151 | self.cache_xref = None |
---|
152 | self.cache_macros = None |
---|
153 | if os.path.exists(dbFilename): |
---|
154 | self.filename = dbFilename |
---|
155 | self.absolute_filename = os.path.abspath(dbFilename) |
---|
156 | self.tokenLog = TokenLog.TokenLog() |
---|
157 | self.tokenLog.processFile(dbFilename) |
---|
158 | self.buf = self.tokenLog.getTokenList() |
---|
159 | self.interpret() |
---|
160 | |
---|
161 | def xrefDict(self, macros = {}): |
---|
162 | ''' |
---|
163 | Make a dictionary that cross-references the 'NAME' field |
---|
164 | between defined and macro-expanded values. Both forward |
---|
165 | and reverse definitions are included. |
---|
166 | |
---|
167 | :param macros: dictionary of macro substitutions. |
---|
168 | :return: cross-reference dictionary |
---|
169 | ''' |
---|
170 | # make this more efficient for repeated calls by caching the xref |
---|
171 | if self.cache_macros == macros: |
---|
172 | return self.cache_xref |
---|
173 | xref = {} |
---|
174 | for k, v in self.pvDict.items(): |
---|
175 | name = utilities.replaceMacros( v['NAME'], macros ) |
---|
176 | xref[k] = name |
---|
177 | xref[name] = k |
---|
178 | self.cache_macros = macros |
---|
179 | self.cache_xref = xref |
---|
180 | return xref |
---|
181 | |
---|
182 | def keyName(self, pvName, macros = {}): |
---|
183 | ''' |
---|
184 | Find the dictionary key that expands to pvName. |
---|
185 | This is easy to get from the cross-reference dictionary. |
---|
186 | (This actually works either way since it uses the xrefDict() method.) |
---|
187 | |
---|
188 | :param macros: dictionary of macro substitutions. |
---|
189 | :return: name of dictionary key or None if not found |
---|
190 | ''' |
---|
191 | try: |
---|
192 | return self.xrefDict(macros)[pvName] |
---|
193 | except: |
---|
194 | return None |
---|
195 | |
---|
196 | def countPVs(self): |
---|
197 | ''' |
---|
198 | :return: number of PVs that have been defined |
---|
199 | ''' |
---|
200 | return len(self.pvDict) |
---|
201 | |
---|
202 | def getPVs(self, macros = {}): |
---|
203 | ''' |
---|
204 | Get a list of the EPICS PVs defined with optional macro substitutions. |
---|
205 | The result with no macro substitutions is trivial. |
---|
206 | |
---|
207 | :param macros: dictionary of macro substitutions. |
---|
208 | :return: list PV names or None if no PVs records have been defined |
---|
209 | ''' |
---|
210 | keys = self.pvDict.keys() |
---|
211 | result = [utilities.replaceMacros( item, macros ) for item in keys] |
---|
212 | return result |
---|
213 | |
---|
214 | def getFull(self, macros = {}): |
---|
215 | ''' |
---|
216 | Get the full specification of the PVs and fields with optional macro substitutions. |
---|
217 | The result with no macro substitutions is trivial. |
---|
218 | |
---|
219 | :param macros: dictionary of macro substitutions. Keys are expanded, also. |
---|
220 | :return: dictionary of dictionaries |
---|
221 | ''' |
---|
222 | result = {} |
---|
223 | for key, fieldDict in self.pvDict.items(): |
---|
224 | k = utilities.replaceMacros( key, macros ) |
---|
225 | result[k] = {} |
---|
226 | for field, value in fieldDict.items(): |
---|
227 | v = utilities.replaceMacros( value, macros ) |
---|
228 | result[k][field] = utilities.replaceMacros( v, macros ) |
---|
229 | return result |
---|
230 | |
---|
231 | def interpret(self): |
---|
232 | ''' |
---|
233 | Interpret the contents of the .db file |
---|
234 | into the internal memory structure. |
---|
235 | |
---|
236 | :raise Exception: |
---|
237 | ''' |
---|
238 | pvDict = {} |
---|
239 | tkn = self.tokenLog.nextActionable() |
---|
240 | while tkn is not None: |
---|
241 | ''' |
---|
242 | :see: http://www.aps.anl.gov/epics/base/R3-14/12-docs/AppDevGuide/node7.html |
---|
243 | |
---|
244 | The Following defines a Record Instance:: |
---|
245 | |
---|
246 | record(record_type,record_name) { |
---|
247 | include "filename" |
---|
248 | field(field_name,"value") |
---|
249 | alias(alias_name) |
---|
250 | info(info_name,"value") |
---|
251 | ... |
---|
252 | } |
---|
253 | alias(record_name,alias_name) |
---|
254 | ''' |
---|
255 | # FIXME: grecord(longin, "$(P)eps:V3104") { field(DESC, "saved VC0") } |
---|
256 | if tkn['tokName'] == 'NAME' and tkn['tokStr'] in ('record', 'grecord'): |
---|
257 | # start of record declaration |
---|
258 | tkn = self.tokenLog.nextActionable() # token with "(" character |
---|
259 | rtyp, name, tkn = self.getTwoItems(tkn) |
---|
260 | fieldDict = { |
---|
261 | 'RTYP': rtyp, # record type |
---|
262 | 'NAME': name # record name |
---|
263 | } |
---|
264 | # trap case where there are NO field declarations |
---|
265 | if not (tkn == None or tkn['tokName'] == 'NAME' and tkn['tokStr'] in ('record', 'grecord')): |
---|
266 | tkn = self.tokenLog.nextActionable() # load the next token |
---|
267 | while tkn['tokStr'] != "}": |
---|
268 | if tkn['tokName'] == 'NAME': |
---|
269 | if tkn['tokStr'] in ('field'): |
---|
270 | # TODO: support 'info' and 'alias' in addition to 'field' |
---|
271 | tkn = self.tokenLog.nextActionable() # "(" character |
---|
272 | argText = tkn['tokLine'].strip()[len('field'):] |
---|
273 | argText = utilities.strip_outer_pair(argText, '(', ')') |
---|
274 | pos = argText.find(",") |
---|
275 | if pos >= 0 and pos < len(argText): |
---|
276 | field = argText[:pos] |
---|
277 | value = utilities.strip_quotes( argText[pos+1:].strip() ) |
---|
278 | fieldDict[field] = value |
---|
279 | tkn = self.advanceToNewLine() |
---|
280 | tkn = self.tokenLog.nextActionable() |
---|
281 | else: |
---|
282 | Exception, "Could not handle this case: " + tkn['tokLine'] |
---|
283 | elif tkn['tokStr'] in ('alias', 'include', 'info'): |
---|
284 | utilities.logMessage(self.absolute_filename) |
---|
285 | utilities.logMessage("line %d" % tkn['start'][0]) |
---|
286 | msg = "ignoring: %s" % tkn['tokLine'].strip("\n") |
---|
287 | utilities.logMessage(msg) |
---|
288 | # skip over this one |
---|
289 | # TODO: this fails for an inline case |
---|
290 | # grecord(longin, "$(P)eps:V3104") { field(DESC, "saved VC0") } |
---|
291 | while tkn['tokName'] != 'NEWLINE': |
---|
292 | tkn = self.tokenLog.next() |
---|
293 | tkn = self.tokenLog.nextActionable() |
---|
294 | else: |
---|
295 | utilities.logMessage(self.absolute_filename) |
---|
296 | utilities.logMessage("line %d" % tkn['start'][0]) |
---|
297 | msg = "unknown: %s" % tkn['tokLine'].strip("\n") |
---|
298 | raise Exception, msg |
---|
299 | else: |
---|
300 | utilities.logMessage(self.absolute_filename) |
---|
301 | utilities.logMessage("line %d" % tkn['start'][0]) |
---|
302 | for k in sorted(fieldDict): |
---|
303 | utilities.logMessage(" fieldDict[%s] = %s" % (k, str(fieldDict[k]))) |
---|
304 | msg = "did not find field: %s" % str(tkn) |
---|
305 | raise Exception, msg |
---|
306 | tkn = self.tokenLog.nextActionable() |
---|
307 | pvDict[name] = fieldDict |
---|
308 | else: |
---|
309 | linenum = tkn['start'][0] |
---|
310 | msg = "(%s,%d) did not find record: %s" % (self.absolute_filename, linenum, str(tkn)) |
---|
311 | raise Exception, msg |
---|
312 | self.pvDict = pvDict |
---|
313 | |
---|
314 | def advanceToNewLine(self): |
---|
315 | ''' |
---|
316 | Move the token pointer to the next NEWLINE token |
---|
317 | |
---|
318 | :return: token after the next NEWLINE token |
---|
319 | ''' |
---|
320 | tkn = self.tokenLog.next() |
---|
321 | while tkn['tokName'] != 'NEWLINE': |
---|
322 | tkn = self.tokenLog.next() |
---|
323 | return tkn |
---|
324 | |
---|
325 | def getTwoItems(self, tkn): |
---|
326 | ''' |
---|
327 | Read a structure of (OP, some:thing-like_this0) |
---|
328 | |
---|
329 | :param tkn: token |
---|
330 | :return: tuple of (word1, word2, tkn) |
---|
331 | ''' |
---|
332 | tkn = self.tokenLog.nextActionable() # load the next token |
---|
333 | word1 = utilities.strip_quotes( tkn['tokStr'] ) # 1st item always 1 token |
---|
334 | tkn = self.tokenLog.nextActionable() # 2nd item might be many tokens |
---|
335 | word2, tkn = self.gatherUpToCloseParen(tkn) |
---|
336 | return word1, word2, tkn |
---|
337 | |
---|
338 | def gatherUpToCloseParen(self, tkn): |
---|
339 | ''' |
---|
340 | Read tokens and accumulate until a ")" character is found |
---|
341 | |
---|
342 | :param tkn: token |
---|
343 | :return: tuple of (word, tkn) |
---|
344 | ''' |
---|
345 | word = "" |
---|
346 | while tkn['tokStr'] != ")": |
---|
347 | # could also watch for breaks in the line position |
---|
348 | if tkn['tokStr'] not in (",", "'", '"'): |
---|
349 | word += tkn['tokStr'] |
---|
350 | tkn = self.tokenLog.nextActionable() |
---|
351 | # next token after ")" |
---|
352 | tkn = self.tokenLog.nextActionable() |
---|
353 | return utilities.strip_quotes( word ), tkn |
---|
354 | |
---|
355 | |
---|
356 | if __name__ == '__main__': |
---|
357 | dbFilename = "../../IOCs/12id/12ida1App/Db/SB_DCM.db" |
---|
358 | macros = {'P': '12ida1:', 'MTH1': 'm9', 'MTH2': 'm11', 'MZ2': 'm14'} |
---|
359 | # .... |
---|
360 | dbFilename = "../../IOCs/12id/iocBoot/ioc12ida1/test.db" |
---|
361 | macros = {'P': 'prj:', |
---|
362 | 'S': 's', |
---|
363 | 'DTYP': 'Asyn Scaler', |
---|
364 | 'FREQ': '50000000', |
---|
365 | 'OUT': '@asyn(mcaSIS3820/1 0)'} |
---|
366 | #dbFilename = "C:\\Users\\Pete\\Apps\\CSS_SoftIOC\\demo\\Ctrl_PID.vdb" |
---|
367 | #macros = {} |
---|
368 | # .... |
---|
369 | db = Db(dbFilename) |
---|
370 | print "Unexpanded list of PVs:" |
---|
371 | for item in sorted(db.getPVs()): |
---|
372 | print item |
---|
373 | print "Expanded list of PVs:" |
---|
374 | for item in sorted(db.getPVs({'P': '12ida1:'})): |
---|
375 | print item |
---|
376 | print 'filename:', db.filename |
---|
377 | print 'absolute filename on this system:', db.absolute_filename |
---|
378 | print 'There are %d PV records defined' % db.countPVs() |
---|
379 | print "Fully expanded list of PVs and fields:" |
---|
380 | pprint.pprint( db.getFull(macros) ) |
---|
381 | print "%s expanded to %s" % (db.keyName('12ida1:DCM:LM2', macros), '12ida1:DCM:LM2') |
---|
382 | print "Cross-reference dictionary:" |
---|
383 | pprint.pprint( db.xrefDict(macros) ) |
---|