• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Module managing the required definitions for using the bits power monitor"""
2
3from datetime import datetime
4import logging
5import os
6import time
7
8from acts import context
9from acts.controllers import power_metrics
10from acts.controllers import power_monitor
11from acts.controllers.bits_lib import bits_client
12from acts.controllers.bits_lib import bits_service
13from acts.controllers.bits_lib import bits_service_config as bsc
14
15MOBLY_CONTROLLER_CONFIG_NAME = 'Bits'
16ACTS_CONTROLLER_REFERENCE_NAME = 'bitses'
17
18
19def create(configs):
20    return [Bits(index, config) for (index, config) in enumerate(configs)]
21
22
23def destroy(bitses):
24    for bits in bitses:
25        bits.teardown()
26
27
28def get_info(bitses):
29    return [bits.config for bits in bitses]
30
31
32def _transform_name(bits_metric_name):
33    """Transform bits metrics names to a more succinct version.
34
35    Examples of bits_metrics_name as provided by the client:
36    - default_device.slider.C1_30__PP0750_L1S_VDD_G3D_M_P:mA,
37    - default_device.slider.C1_30__PP0750_L1S_VDD_G3D_M_P:mW,
38    - default_device.Monsoon.Monsoon:mA,
39    - default_device.Monsoon.Monsoon:mW,
40    - <device>.<collector>.<rail>:<unit>
41
42    Args:
43        bits_metric_name: A bits metric name.
44
45    Returns:
46        For monsoon metrics, and for backwards compatibility:
47          Monsoon:mA -> avg_current,
48          Monsoon:mW -> avg_power,
49
50        For everything else:
51          <rail>:mW -> <rail/rail>_avg_current
52          <rail>:mW -> <rail/rail>_avg_power
53          ...
54    """
55    prefix, unit = bits_metric_name.split(':')
56    rail = prefix.split('.')[-1]
57
58    if 'mW' == unit:
59        suffix = 'avg_power'
60    elif 'mA' == unit:
61        suffix = 'avg_current'
62    elif 'mV' == unit:
63        suffix = 'avg_voltage'
64    else:
65        logging.getLogger().warning('unknown unit type for unit %s' % unit)
66        suffix = ''
67
68    if 'Monsoon' == rail:
69        return suffix
70    elif suffix == '':
71        return rail
72    else:
73        return '%s_%s' % (rail, suffix)
74
75
76def _raw_data_to_metrics(raw_data_obj):
77    data = raw_data_obj['data']
78    metrics = []
79    for sample in data:
80        unit = sample['unit']
81        if 'Msg' == unit:
82            continue
83        elif 'mW' == unit:
84            unit_type = 'power'
85        elif 'mA' == unit:
86            unit_type = 'current'
87        elif 'mV' == unit:
88            unit_type = 'voltage'
89        else:
90            logging.getLogger().warning('unknown unit type for unit %s' % unit)
91            continue
92
93        name = _transform_name(sample['name'])
94        avg = sample['avg']
95        metrics.append(power_metrics.Metric(avg, unit_type, unit, name=name))
96
97    return metrics
98
99
100def _get_single_file(registry, key):
101    if key not in registry:
102        return None
103    entry = registry[key]
104    if isinstance(entry, str):
105        return entry
106    if isinstance(entry, list):
107        return None if len(entry) == 0 else entry[0]
108    raise ValueError('registry["%s"] is of unsupported type %s for this '
109                     'operation. Supported types are str and list.' % (
110                         key, type(entry)))
111
112
113class Bits(object):
114    def __init__(self, index, config):
115        """Creates an instance of a bits controller.
116
117        Args:
118            index: An integer identifier for this instance, this allows to
119                tell apart different instances in the case where multiple
120                bits controllers are being used concurrently.
121            config: The config as defined in the ACTS  BiTS controller config.
122                Expected format is:
123                {
124                    // optional
125                    'Monsoon':   {
126                        'serial_num': <serial number:int>,
127                        'monsoon_voltage': <voltage:double>
128                    }
129                    // optional
130                    'Kibble': [
131                        {
132                            'board': 'BoardName1',
133                            'connector': 'A',
134                            'serial': 'serial_1'
135                        },
136                        {
137                            'board': 'BoardName2',
138                            'connector': 'D',
139                            'serial': 'serial_2'
140                        }
141                    ]
142                }
143        """
144        self.index = index
145        self.config = config
146        self._service = None
147        self._client = None
148
149    def setup(self, *_, registry=None, **__):
150        """Starts a bits_service in the background.
151
152        This function needs to be
153        Args:
154            registry: A dictionary with files used by bits. Format:
155                {
156                    // required, string or list of strings
157                    bits_service: ['/path/to/bits_service']
158
159                    // required, string or list of strings
160                    bits_client: ['/path/to/bits.par']
161
162                    // needed for monsoon, string or list of strings
163                    lvpm_monsoon: ['/path/to/lvpm_monsoon.par']
164
165                    // needed for monsoon, string or list of strings
166                    hvpm_monsoon: ['/path/to/hvpm_monsoon.par']
167
168                    // needed for kibble, string or list of strings
169                    kibble_bin: ['/path/to/kibble.par']
170
171                    // needed for kibble, string or list of strings
172                    kibble_board_file: ['/path/to/phone_s.board']
173
174                    // optional, string or list of strings
175                    vm_file: ['/path/to/file.vm']
176                }
177
178                All fields in this dictionary can be either a string or a list
179                of strings. If lists are passed, only their first element is
180                taken into account. The reason for supporting lists but only
181                acting on their first element is for easier integration with
182                harnesses that handle resources as lists.
183        """
184        if registry is None:
185            registry = power_monitor.get_registry()
186        if 'bits_service' not in registry:
187            raise ValueError('No bits_service binary has been defined in the '
188                             'global registry.')
189        if 'bits_client' not in registry:
190            raise ValueError('No bits_client binary has been defined in the '
191                             'global registry.')
192
193        bits_service_binary = _get_single_file(registry, 'bits_service')
194        bits_client_binary = _get_single_file(registry, 'bits_client')
195        lvpm_monsoon_bin = _get_single_file(registry, 'lvpm_monsoon')
196        hvpm_monsoon_bin = _get_single_file(registry, 'hvpm_monsoon')
197        kibble_bin = _get_single_file(registry, 'kibble_bin')
198        kibble_board_file = _get_single_file(registry, 'kibble_board_file')
199        vm_file = _get_single_file(registry, 'vm_file')
200        config = bsc.BitsServiceConfig(self.config,
201                                       lvpm_monsoon_bin=lvpm_monsoon_bin,
202                                       hvpm_monsoon_bin=hvpm_monsoon_bin,
203                                       kibble_bin=kibble_bin,
204                                       kibble_board_file=kibble_board_file,
205                                       virtual_metrics_file=vm_file)
206        output_log = os.path.join(
207            context.get_current_context().get_full_output_path(),
208            'bits_service_out_%s.txt' % self.index)
209        service_name = 'bits_config_%s' % self.index
210
211        self._service = bits_service.BitsService(config,
212                                                 bits_service_binary,
213                                                 output_log,
214                                                 name=service_name,
215                                                 timeout=3600 * 24)
216        self._service.start()
217        self._client = bits_client.BitsClient(bits_client_binary,
218                                              self._service,
219                                              config)
220        # this call makes sure that the client can interact with the server.
221        devices = self._client.list_devices()
222        logging.getLogger().debug(devices)
223
224    def disconnect_usb(self, *_, **__):
225        self._client.disconnect_usb()
226
227    def connect_usb(self, *_, **__):
228        self._client.connect_usb()
229
230    def measure(self, *_, measurement_args=None, **__):
231        """Blocking function that measures power through bits for the specified
232        duration. Results need to be consulted through other methods such as
233        get_metrics or export_to_csv.
234
235        Args:
236            measurement_args: A dictionary with the following structure:
237                {
238                   'duration': <seconds to measure for>
239                }
240        """
241        if measurement_args is None:
242            raise ValueError('measurement_args can not be left undefined')
243
244        duration = measurement_args.get('duration')
245        if duration is None:
246            raise ValueError(
247                'duration can not be left undefined within measurement_args')
248        self._client.start_collection()
249        time.sleep(duration)
250
251    def get_metrics(self, *_, timestamps=None, **__):
252        """Gets metrics for the segments delimited by the timestamps dictionary.
253
254        Args:
255            timestamps: A dictionary of the shape:
256                {
257                    'segment_name': {
258                        'start' : <milliseconds_since_epoch> or <datetime>
259                        'end': <milliseconds_since_epoch> or <datetime>
260                    }
261                    'another_segment': {
262                        'start' : <milliseconds_since_epoch> or <datetime>
263                        'end': <milliseconds_since_epoch> or <datetime>
264                    }
265                }
266        Returns:
267            A dictionary of the shape:
268                {
269                    'segment_name': <list of power_metrics.Metric>
270                    'another_segment': <list of power_metrics.Metric>
271                }
272        """
273        if timestamps is None:
274            raise ValueError('timestamps dictionary can not be left undefined')
275
276        metrics = {}
277
278        for segment_name, times in timestamps.items():
279            start = times['start']
280            end = times['end']
281
282            # bits accepts nanoseconds only, but since this interface needs to
283            # backwards compatible with monsoon which works with milliseconds we
284            # require to do a conversion from milliseconds to nanoseconds.
285            # The preferred way for new calls to this function should be using
286            # datetime instead which is unambiguous
287            if isinstance(start, (int, float)):
288                start = times['start'] * 1e6
289            if isinstance(end, (int, float)):
290                end = times['end'] * 1e6
291
292            self._client.add_marker(start, 'start - %s' % segment_name)
293            self._client.add_marker(end, 'end - %s' % segment_name)
294            raw_metrics = self._client.get_metrics(start, end)
295            metrics[segment_name] = _raw_data_to_metrics(raw_metrics)
296        return metrics
297
298    def release_resources(self):
299        self._client.stop_collection()
300
301    def teardown(self):
302        if self._service is None:
303            return
304
305        if self._service.service_state == bits_service.BitsServiceStates.STARTED:
306            self._service.stop()
307