blob: 4ee54fe885d4cf4e4b7073edbc630e4fc699dc9f [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""System configuration module."""
import collections
import logging
import os
import xml.etree.ElementTree
# valid tags in system config xml. Any others will be ignored
SYSCFG_TAG_LIST = ['map', 'control']
ALLOWABLE_INPUT_TYPES = {'float': float, 'int': int, 'str': str}
class SystemConfigError(Exception):
"""Error class for SystemConfig."""
class SystemConfig(object):
"""SystemConfig Class.
System config files describe how to talk to various pieces on the device under
test. The system config may be broken up into multiple file to make it easier
to share configs among similar DUTs. This class has the support to take in
multiple SystemConfig files and treat them as one unified structure
SystemConfig files are written in xml and consist of four main elements
0. Include : Ability to include other config files
<include>
<name>servo_loc.xml</name>
</include>
NOTE, All includes in a file WILL be sourced prior to any other elements in
the XML.
1. Map : Allow user-friendly naming for things to abstract
certain things like on=0 for things that are assertive low on
actual h/w
<map>
<name>onoff_i</name>
<doc>assertive low map for on/off</doc>
<params on="0" off="1"></params>
</map>
2. Control : Bulk of the system file. These elements are
typically gpios, adcs, dacs which allow either control or sampling
of events on the h/w. Controls should have a 1to1 correspondence
with hardware elements between control system and DUT.
<control>
<name>warm_reset</name>
<doc>Reset the device warmly</doc>
<params interface="1" drv="gpio" offset="5" map="onoff_i"></params>
</control>
TODO(tbroch) Implement sequence or deprecate
3. Sequence : List of control calls to create a desired
configuration of h/w. These could certainly be done by writing
simple scripts to send individual control calls to the server but
encapsulating them into the system file should allow for tighter
control of the sequence ... especially if timing of the sequence
is paramount.
<sequence>
<name>i2c_mux_seq</name>
<cmdlist>i2c_mux_en:off i2c_mux_add:__arg0__ i2c_mux_en:on</cmdlist>
</sequence>
Public Attributes:
syscfg_dict: 3-deep dictionary created when parsing system files. Its
organized as [tag][name][type] where:
tag: map | control | sequence
name: string name of tag element
type: data type of payload either, doc | get | set presently
doc: string describing the map,control or sequence
get: a dictionary for getting values from named control
set: a dictionary for setting values to named control
hwinit: list of control tuples (name, value) to be initialized in order
Private Attributes:
_loaded_xml_files: set of filenames already loaded to avoid sourcing XML
multiple times.
"""
def __init__(self):
"""SystemConfig constructor."""
self._logger = logging.getLogger('SystemConfig')
self._logger.debug('')
self.syscfg_dict = collections.defaultdict(dict)
self.hwinit = []
self._loaded_xml_files = []
def find_cfg_file(self, filename):
"""Find the filename for a system XML config file.
If the provided `filename` names a valid file, use that.
Otherwise, `filename` must name a file in the 'data'
subdirectory stored with this module.
Returns the path selected as described above; if neither of the
paths names a valid file, return `None`.
Args:
filename: string of path to system file ( xml )
"""
if os.path.isfile(filename):
return filename
default_path = os.path.join(os.path.dirname(__file__), 'data')
fullname = os.path.join(default_path, filename)
if os.path.isfile(fullname):
return fullname
return None
def add_cfg_file(self, filename, name_prefix=None, interface_increment=0):
"""Add system config file to the system config object
Each design may rely on multiple system files so need to have the facility
to parse them all.
For example, we may have a:
1. default for all controls that are the same for each of the
control systems
2. default for a particular DUT system's usage across the
connector
3. specific one for particular version of DUT (evt,dvt,mp)
4. specific one for a one-off rework done to a system
Special key parameters in config files:
clobber_ok: signifies this control may _clobber_ an existing definition
of the same name. Note, its value is ignored ( clobber_ok='' )
NOTE, method is recursive when parsing 'include' elements from XML.
The arguments name_prefix and interface_increment are used to support
multiple servo micros. The interfaces of the extra servo micros, like
the one for hammer, are relocated to higher slots. The controls of this
extra servo micros should be shifted their interface numbers. Adding
the name prefix avoid conflict with the main servo micro.
Args:
filename: string of path to system file ( xml )
name_prefix: string to prepend to all control names
interface_increment: number to add to all interfaces
Raises:
SystemConfigError: for schema violations, or file not found.
"""
cfgname = self.find_cfg_file(filename)
if not cfgname:
msg = 'Unable to find system file %s' % filename
self._logger.error(msg)
raise SystemConfigError(msg)
filename = cfgname
if (filename, name_prefix, interface_increment) in self._loaded_xml_files:
self._logger.warn('Already sourced system file (%s, %s, %d).', filename,
name_prefix, interface_increment)
return
self._loaded_xml_files.append((filename, name_prefix, interface_increment))
self._logger.info('Loading XML config (%s, %s, %d)', filename, name_prefix,
interface_increment)
root = xml.etree.ElementTree.parse(filename).getroot()
for element in root.findall('include'):
self.add_cfg_file(
element.find('name').text, name_prefix, interface_increment)
for tag in SYSCFG_TAG_LIST:
for element in root.findall(tag):
element_str = xml.etree.ElementTree.tostring(element)
try:
name = element.find('name').text
if tag == 'control' and name_prefix:
name = name_prefix + '_' + name
except AttributeError:
# TODO(tbroch) would rather have lineno but dumping element seems
# better than nothing. Utimately a DTD/XSD for the XML schema will
# catch these anyways.
raise SystemConfigError('%s: no name ... see XML\n%s' % (tag,
element_str))
try:
doc = ' '.join(element.find('doc').text.split())
except AttributeError:
doc = 'undocumented'
try:
alias = element.find('alias').text
except AttributeError:
alias = None
try:
remap = element.find('remap').text
except AttributeError:
remap = None
if remap:
try:
self.syscfg_dict[tag][remap] = self.syscfg_dict[tag][name]
except KeyError:
# Sometimes the remap control doesn't exist (e.g. fw_up in servo
# v4). Just ignore it and continue on.
pass
continue
get_dict = None
set_dict = None
clobber_ok = False
params_list = element.findall('params')
# Modify the interface attributes.
for params in params_list:
if 'interface' in params.attrib:
if params.attrib['interface'] != 'servo':
interface_id = int(params.attrib['interface'])
params.attrib['interface'] = interface_id + interface_increment
if len(params_list) == 2:
assert tag != 'map', 'maps have only one params entry'
for params in params_list:
if 'cmd' not in params.attrib:
raise SystemConfigError('%s %s multiple params but no cmd\n%s' %
(tag, name, element_str))
cmd = params.attrib['cmd']
if cmd == 'get':
if get_dict:
raise SystemConfigError(
'%s %s multiple get params defined\n%s' % (tag, name,
element_str))
get_dict = params.attrib
elif cmd == 'set':
if set_dict:
raise SystemConfigError(
'%s %s multiple set params defined\n%s' % (tag, name,
element_str))
set_dict = params.attrib
else:
raise SystemConfigError("%s %s cmd of 'get'|'set' not found\n%s" %
(tag, name, element_str))
elif len(params_list) == 1:
get_dict = params_list[0].attrib
set_dict = get_dict
else:
raise SystemConfigError('%s %s has illegal number of params %d\n%s' %
(tag, name, len(params_list), element_str))
# Save the control name to the params dicts, such that the driver can
# refer to it.
if tag == 'control':
get_dict['control_name'] = name
set_dict['control_name'] = name
clobber_ok = ('clobber_ok' in set_dict or 'clobber_ok' in get_dict)
if (tag == 'control' and name in self.syscfg_dict[tag] and
not clobber_ok):
raise SystemConfigError(
"Duplicate %s %s without 'clobber_ok' key\n%s" % (tag, name,
element_str))
if tag == 'map':
self.syscfg_dict[tag][name] = {'doc': doc, 'map_params': get_dict}
if alias:
raise SystemConfigError('No aliases for maps allowed')
continue
if 'init' in set_dict:
hwinit_found = False
# only allow one hwinit per control
if clobber_ok:
for i, (hwinit_name, _) in enumerate(self.hwinit):
if hwinit_name == name:
self.hwinit[i] = (name, set_dict['init'])
hwinit_found = True
break
if not hwinit_found:
self.hwinit.append((name, set_dict['init']))
if clobber_ok and name in self.syscfg_dict[tag]:
# it's an existing control
self.syscfg_dict[tag][name]['get_params'].update(get_dict)
self.syscfg_dict[tag][name]['set_params'].update(set_dict)
if doc != 'undocumented':
self.syscfg_dict[tag][name]['doc'] = doc
else:
# it's a new control
self.syscfg_dict[tag][name] = {
'doc': doc,
'get_params': get_dict,
'set_params': set_dict
}
if alias:
for aliasname in (elem.strip() for elem in alias.split(',')):
if name_prefix:
aliasname = name_prefix + '_' + aliasname
self.syscfg_dict[tag][aliasname] = self.syscfg_dict[tag][name]
def lookup_control_params(self, name, is_get=True):
"""Lookup & return control parameter dictionary.
Note, controls must have either one or two set of parameters. In the case
of two, the dictionary must contain k=v element of 'type':'get' or
'type':'set'
Args:
name: string of control name to lookup
isget: boolean of whether params should be for 'get' | 'set'
Returns:
control's parameter dictionary for approrpiate get or set
Raises:
NameError: if control name not found
SystemConfigError: if error encountered identifying parameters
"""
if name not in self.syscfg_dict['control']:
raise NameError('No control named %s. All controls:\n%s' %
(name,
','.join(sorted(self.syscfg_dict['control'].keys()))))
if is_get:
return self.syscfg_dict['control'][name]['get_params']
else:
return self.syscfg_dict['control'][name]['set_params']
def is_control(self, name):
"""Determine if name is a control or not.
Args:
name: string of control name to lookup
Returns boolean, True if name is control, False otherwise
"""
return name in self.syscfg_dict['control']
def get_control_docstring(self, name):
"""Get controls doc string.
Args:
name: string of control name to lookup
Returns:
doc string of the control
"""
return self.syscfg_dict['control'][name]['doc']
def _lookup(self, tag, name_str):
"""Lookup the tag name_str and return dictionary or None if not found.
Args:
tag: string of tag (from SYSCFG_TAG_LIST) to look for name_str under.
name_str: string of name to lookup
Returns:
dictionary from syscfg_dict[tag][name_str] or None
"""
self._logger.debug('lookup of %s %s' % (tag, name_str))
return self.syscfg_dict[tag].get(name_str)
def resolve_val(self, params, map_vstr):
"""Resolve string value.
Values to set the control to can be mapped to symbolic strings for better
readability. For example, its difficult to remember assertion levels of
various gpios. Maps allow things like 'reset:on'. Also provides
abstraction so that assertion level doesn't have to be exposed.
Args:
params: parameters dictionary for control
map_vstr: string thats acceptable values are:
an int (can be "DECIMAL", "0xHEX", 0OCT", or "0bBINARY".
a floating point value.
an alphanumeric which is key in the corresponding map dictionary.
Returns:
Resolved value as float or int
Raises:
SystemConfigError: mapping issues found
"""
if 'input_type' in params:
if params['input_type'] in ALLOWABLE_INPUT_TYPES:
input_type = ALLOWABLE_INPUT_TYPES[params['input_type']]
return input_type(map_vstr)
else:
self._logger.error('Unrecognized input type.')
else:
# TODO(tbroch): deprecate below once all controls have input_type params
try:
return int(str(map_vstr), 0)
except ValueError:
pass
try:
return float(str(map_vstr))
except ValueError:
pass
# its a map
if 'map' not in params:
raise SystemConfigError('No map for control but value is a string')
map_dict = self._lookup('map', params['map'])
if map_dict is None:
raise SystemConfigError("Map %s isn't defined" % params['map'])
try:
map_value = map_dict['map_params'][map_vstr]
except KeyError:
raise SystemConfigError(
("Map %s doesn't contain key %s\n" + "Try one of -> '%s'") %
(params['map'], map_vstr, "', '".join(map_dict['map_params'].keys())))
# TODO(tbroch) likely that maps are only integers but what if ...
return int(map_value, 0)
def _Fmt_hex(self, int_val):
"""Format integer into hex.
Args:
int_val: integer to be formatted into hex string
Returns:
string of integer in hex format
"""
return hex(int_val)
def reformat_val(self, params, value):
"""Reformat value.
Formatting determined via:
1. if has map, then remap
2. else if has fmt param, use that function
Args:
params: parameter dictionary for control
value: value to reformat
Returns:
formatted string value if reformatting needed
value otherwise
Raises:
SystemConfigError: errors using formatting param
"""
# TODO(crbug.com/841097): revisit logic for value here once
# resolution found on bug.
if value is not None and 'map' not in params and 'fmt' not in params:
return value
reformat_value = str(value)
if 'map' in params:
map_dict = self._lookup('map', params['map'])
if map_dict:
for keyname, val in map_dict['map_params'].iteritems():
if val == reformat_value:
reformat_value = keyname
break
elif 'fmt' in params:
fmt = params['fmt']
try:
func = getattr(self, '_Fmt_%s' % fmt)
except AttributeError:
raise SystemConfigError('Unrecognized format %s' % fmt)
try:
reformat_value = func(value)
except Exception:
raise SystemConfigError('Problem executing format %s' % fmt)
return reformat_value
def display_config(self, tag=None):
"""Display human-readable values of map, control, or sequence.
Args:
tag : string of either 'map' | 'control' | 'sequence' or None for all
Returns:
string to be displayed.
"""
rsp = []
if tag is None:
tag_list = SYSCFG_TAG_LIST
else:
tag_list = [tag]
for tag in sorted(tag_list):
rsp.append('*************')
rsp.append('* ' + tag.upper())
rsp.append('*************')
max_len = 0
max_len = max(len(name) for name in self.syscfg_dict[tag].iterkeys())
dashes = '-' * max_len
for name in sorted(self.syscfg_dict[tag].iterkeys()):
item_dict = self.syscfg_dict[tag][name]
padded_name = '%-*s' % (max_len, name)
rsp.append('%s DOC: %s' % (padded_name, item_dict['doc']))
if tag == 'map':
rsp.append('%s MAP: %s' % (dashes, str(item_dict['map_params'])))
else:
rsp.append('%s GET: %s' % (dashes, str(item_dict['get_params'])))
rsp.append('%s SET: %s' % (dashes, str(item_dict['set_params'])))
return '\n'.join(rsp)
def test():
"""Integration test.
TODO(tbroch) Enhance integration test and add unittest (see mox)
"""
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - ' + '%(levelname)s - %(message)s')
scfg = SystemConfig()
# TODO(tbroch) make this a comprenhensive test xml file
scfg.add_cfg_file(os.path.join('data', 'servo.xml'))
scfg.display_config()
control_dict = scfg._lookup('control', 'goog_rec_mode')
# checking mapping functionality
control_params = control_dict['get_params']
control_params = control_dict['set_params']
if 'map' in control_params:
map_name = control_params['map']
map_dict = scfg._lookup('map', map_name)
if not map_dict:
raise Exception('Unable to find map %s', map_name)
logging.info('')
for keyname, val in map_dict['map_params'].iteritems():
resolved_val = str(scfg.resolve_val(control_params, keyname))
if resolved_val != val:
logging.error('resolve failed for %s -> %s != %s', keyname, val,
resolved_val)
# try re-mapping
reformat_val = scfg.reformat_val(control_params, int(val))
reformat_val = scfg.reformat_val(control_params, val)
if reformat_val != keyname:
logging.error('reformat failed for %s -> %s != %s', val, keyname,
reformat_val)
if __name__ == '__main__':
test()