1# Lint as: python2, python3
2# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A Python library to interact with INA219 module for TPM testing.
7
8Background
9 - INA219 is one of two modules on TTCI board
10 - This library provides methods to interact with INA219 programmatically
11
12Dependency
13 - This library depends on a new C shared library called "libsmogcheck.so".
14 - In order to run test cases built using this API, one needs a TTCI board
15
16Notes:
17 - An exception is raised if it doesn't make logical sense to continue program
18   flow (e.g. I/O error prevents test case from executing)
19 - An exception is caught and then converted to an error code if the caller
20   expects to check for error code per API definition
21"""
22
23from __future__ import absolute_import
24from __future__ import division
25from __future__ import print_function
26
27import logging, re
28from autotest_lib.client.common_lib import i2c_node
29import six
30
31
32# INA219 registers
33INA_REG = {
34    'CONF': 0,        # Configuration Register
35    'SHUNT_VOLT': 1,  # Shunt Voltage
36    'BUS_VOLT': 2,    # Bus Voltage
37    'POWER': 3,       # Power
38    'CURRENT': 4,     # Current
39    'CALIB': 5,       # Calibration
40    }
41
42# Regex pattern for measurement value
43HEX_STR_PATTERN = re.compile('^0x([0-9a-f]{2})([0-9a-f]{2})$')
44
45# Constants used to initialize INA219 registers
46# TODO(tgao): add docstring for these values after stevenh replies
47INA_CONF_INIT_VAL = 0x9f31
48INA_CALIB_INIT_VAL = 0xc90e
49
50# Default values used to calculate/interpret voltage and current measurements.
51DEFAULT_MEAS_RANGE_VALUE = {
52    'current': {'max': 0.1, 'min': 0.0, 'denom': 10000.0,
53                'reg': INA_REG['CURRENT']},
54    'voltage': {'max': 3.35, 'min': 3.25, 'denom': 2000.0,
55                'reg': INA_REG['BUS_VOLT']},
56    }
57
58
59class InaError(Exception):
60    """Base class for all errors in this module."""
61
62
63class InaController(i2c_node.I2cNode):
64    """Object to control INA219 module on TTCI board."""
65
66    def __init__(self, node_addr=None, range_dict=None):
67        """Constructor.
68
69        Mandatory params:
70          node_addr: node address to set. Default: None.
71
72        Optional param:
73          range_dict: desired max/min thresholds for measurement values.
74                      Default: DEFAULT_MEAS_RANGE_VALUE.
75
76        Args:
77          node_addr: an integer, address of main or backup power.
78          range_dict: desired max/min thresholds for measurement values.
79
80        Raises:
81          InaError: if error initializing INA219 module or invalid range_dict.
82        """
83        super(InaController, self).__init__()
84        if node_addr is None:
85            raise InaError('Error node_addr expected')
86
87        try:
88            if range_dict is None:
89                range_dict = DEFAULT_MEAS_RANGE_VALUE
90            else:
91                self._validateRangeDict(DEFAULT_MEAS_RANGE_VALUE, range_dict)
92            self.range_dict = range_dict
93
94            self.setNodeAddress(node_addr)
95            self.writeWord(INA_REG['CONF'], INA_CONF_INIT_VAL)
96            self.writeWord(INA_REG['CALIB'], INA_CALIB_INIT_VAL)
97        except InaError as e:
98            raise InaError('Error initializing INA219: %s' % e)
99
100    def _validateRangeDict(self, d_ref, d_in):
101        """Validates keys and types of value in range_dict.
102
103        Iterate over d_ref to make sure all keys exist in d_in and
104        values are of the correct type.
105
106        Args:
107          d_ref: a dictionary, used as reference.
108          d_in: a dictionary, to be validated against reference.
109
110        Raises:
111          InaError: if range_dict is invalid.
112        """
113        for k, v in six.iteritems(d_ref):
114            if k not in d_in:
115                raise InaError('Key %s not present in dict %r' % (k, d_in))
116            if type(v) != type(d_in[k]):
117                raise InaError(
118                    'Value type mismatch for key %s. Expected: %s; actual = %s'
119                    % (k, type(v), type(d_in[k])))
120            if type(v) is dict:
121                self._validateRangeDict(v, d_in[k])
122
123    def readMeasure(self, measure):
124        """Reads requested measurement.
125
126        Args:
127          measure: a string, 'current' or 'voltage'.
128
129        Returns:
130          a float, measurement in native units. Or None if error.
131
132        Raises:
133          InaError: if error reading requested measurement.
134        """
135        try:
136            hex_str = '0x%.4x' % self.readWord(self.range_dict[measure]['reg'])
137            logging.debug('Word read = %r', hex_str)
138            return self._checkMeasureRange(hex_str, measure)
139        except InaError as e:
140            logging.error('Error reading %s: %s', measure, e)
141
142    def getPowerMetrics(self):
143        """Get measurement metrics for Main Power.
144
145        Returns:
146          an integer, 0 for success and -1 for error.
147          a float, voltage value in Volts. Or None if error.
148          a float, current value in Amps. Or None if error.
149        """
150        logging.info('Attempt to get power metrics')
151        try:
152            return (0, self.readMeasure('voltage'),
153                    self.readMeasure('current'))
154        except InaError as e:
155            logging.error('getPowerMetrics(): %s', e)
156            return (-1, None, None)
157
158    def _checkMeasureRange(self, hex_str, measure):
159        """Checks if measurement value falls within a pre-specified range.
160
161        Args:
162          hex_str: a string (hex value).
163          measure: a string, 'current' or 'voltage'.
164
165        Returns:
166          measure_float: a float, measurement value.
167
168        Raises:
169          InaError: if value doesn't fall in range.
170        """
171        measure_float = self._convertHexToFloat(
172            hex_str, self.range_dict[measure]['denom'])
173        measure_msg = '%s value %.2f' % (measure, measure_float)
174        range_msg = '[%(min).2f, %(max).2f]' % self.range_dict[measure]
175        if (measure_float < self.range_dict[measure]['min'] or
176            measure_float > self.range_dict[measure]['max']):
177            raise InaError('%s is out of range %s' % measure_msg, range_msg)
178        logging.info('%s is in range %s', measure_msg, range_msg)
179        return measure_float
180
181    def _convertHexToFloat(self, hex_str, denom):
182        """Performs measurement calculation.
183
184        The measurement reading from INA219 module is a 2-byte hex string.
185        To convert this hex string to a float, we need to swap these two bytes
186        and perform a division. An example:
187          response = 0xca19
188          swap bytes to get '0x19ca'
189          convert to decimal value = 6602
190          divide decimal by 2000.0 = 3.301 (volts)
191
192        Args:
193          hex_str: a string (raw hex value).
194          denom: a float, denominator used for hex-to-float conversion.
195
196        Returns:
197          a float, measurement value.
198
199        Raises:
200          InaError: if error converting measurement to float.
201        """
202        match = HEX_STR_PATTERN.match(hex_str)
203        if not match:
204            raise InaError('Error: hex string %s does not match '
205                           'expected pattern' % hex_str)
206
207        decimal = int('0x%s%s' % (match.group(2), match.group(1)), 16)
208        return decimal/denom
209