• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Module managing the required definitions for using the bits power monitor"""
2
3import logging
4import os
5import time
6import uuid
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
32class BitsError(Exception):
33    pass
34
35
36class _BitsCollection(object):
37    """Object that represents a bits collection
38
39    Attributes:
40        name: The name given to the collection.
41        markers_buffer: An array of un-flushed markers, each marker is
42        represented by a bi-dimensional tuple with the format
43        (<nanoseconds_since_epoch or datetime>, <text>).
44        monsoon_output_path: A path to store monsoon-like data if possible, Bits
45        uses this path to attempt data extraction in monsoon format, if this
46        parameter is left as None such extraction is not attempted.
47    """
48
49    def __init__(self, name, monsoon_output_path=None):
50        self.monsoon_output_path = monsoon_output_path
51        self.name = name
52        self.markers_buffer = []
53
54    def add_marker(self, timestamp, marker_text):
55        self.markers_buffer.append((timestamp, marker_text))
56
57
58def _transform_name(bits_metric_name):
59    """Transform bits metrics names to a more succinct version.
60
61    Examples of bits_metrics_name as provided by the client:
62    - default_device.slider.C1_30__PP0750_L1S_VDD_G3D_M_P:mA,
63    - default_device.slider.C1_30__PP0750_L1S_VDD_G3D_M_P:mW,
64    - default_device.Monsoon.Monsoon:mA,
65    - default_device.Monsoon.Monsoon:mW,
66    - <device>.<collector>.<rail>:<unit>
67
68    Args:
69        bits_metric_name: A bits metric name.
70
71    Returns:
72        For monsoon metrics, and for backwards compatibility:
73          Monsoon:mA -> avg_current,
74          Monsoon:mW -> avg_power,
75
76        For everything else:
77          <rail>:mW -> <rail/rail>_avg_current
78          <rail>:mW -> <rail/rail>_avg_power
79          ...
80    """
81    prefix, unit = bits_metric_name.split(':')
82    rail = prefix.split('.')[-1]
83
84    if 'mW' == unit:
85        suffix = 'avg_power'
86    elif 'mA' == unit:
87        suffix = 'avg_current'
88    elif 'mV' == unit:
89        suffix = 'avg_voltage'
90    else:
91        logging.warning('unknown unit type for unit %s' % unit)
92        suffix = ''
93
94    if 'Monsoon' == rail:
95        return suffix
96    elif suffix == '':
97        return rail
98    else:
99        return '%s_%s' % (rail, suffix)
100
101
102def _raw_data_to_metrics(raw_data_obj):
103    data = raw_data_obj['data']
104    metrics = []
105    for sample in data:
106        unit = sample['unit']
107        if 'Msg' == unit:
108            continue
109        elif 'mW' == unit:
110            unit_type = 'power'
111        elif 'mA' == unit:
112            unit_type = 'current'
113        elif 'mV' == unit:
114            unit_type = 'voltage'
115        else:
116            logging.warning('unknown unit type for unit %s' % unit)
117            continue
118
119        name = _transform_name(sample['name'])
120        avg = sample['avg']
121        metrics.append(power_metrics.Metric(avg, unit_type, unit, name=name))
122
123    return metrics
124
125
126def _get_single_file(registry, key):
127    if key not in registry:
128        return None
129    entry = registry[key]
130    if isinstance(entry, str):
131        return entry
132    if isinstance(entry, list):
133        return None if len(entry) == 0 else entry[0]
134    raise ValueError('registry["%s"] is of unsupported type %s for this '
135                     'operation. Supported types are str and list.' % (
136                         key, type(entry)))
137
138
139class Bits(object):
140
141    ROOT_RAIL_KEY = 'RootRail'
142    ROOT_RAIL_DEFAULT_VALUE = 'Monsoon:mA'
143
144    def __init__(self, index, config):
145        """Creates an instance of a bits controller.
146
147        Args:
148            index: An integer identifier for this instance, this allows to
149                tell apart different instances in the case where multiple
150                bits controllers are being used concurrently.
151            config: The config as defined in the ACTS  BiTS controller config.
152                Expected format is:
153                {
154                    // optional
155                    'Monsoon':   {
156                        'serial_num': <serial number:int>,
157                        'monsoon_voltage': <voltage:double>
158                    }
159                    // optional
160                    'Kibble': [
161                        {
162                            'board': 'BoardName1',
163                            'connector': 'A',
164                            'serial': 'serial_1'
165                        },
166                        {
167                            'board': 'BoardName2',
168                            'connector': 'D',
169                            'serial': 'serial_2'
170                        }
171                    ]
172                    // optional
173                    'RootRail': 'Monsoon:mA'
174                }
175        """
176        self.index = index
177        self.config = config
178        self._service = None
179        self._client = None
180        self._active_collection = None
181        self._collections_counter = 0
182        self._root_rail = config.get(self.ROOT_RAIL_KEY,
183                                     self.ROOT_RAIL_DEFAULT_VALUE)
184
185    def setup(self, *_, registry=None, **__):
186        """Starts a bits_service in the background.
187
188        This function needs to be called with either a registry or after calling
189        power_monitor.update_registry, and it needs to be called before any other
190        method in this class.
191
192        Args:
193            registry: A dictionary with files used by bits. Format:
194                {
195                    // required, string or list of strings
196                    bits_service: ['/path/to/bits_service']
197
198                    // required, string or list of strings
199                    bits_client: ['/path/to/bits.par']
200
201                    // needed for monsoon, string or list of strings
202                    lvpm_monsoon: ['/path/to/lvpm_monsoon.par']
203
204                    // needed for monsoon, string or list of strings
205                    hvpm_monsoon: ['/path/to/hvpm_monsoon.par']
206
207                    // needed for kibble, string or list of strings
208                    kibble_bin: ['/path/to/kibble.par']
209
210                    // needed for kibble, string or list of strings
211                    kibble_board_file: ['/path/to/phone_s.board']
212
213                    // optional, string or list of strings
214                    vm_file: ['/path/to/file.vm']
215                }
216
217                All fields in this dictionary can be either a string or a list
218                of strings. If lists are passed, only their first element is
219                taken into account. The reason for supporting lists but only
220                acting on their first element is for easier integration with
221                harnesses that handle resources as lists.
222        """
223        if registry is None:
224            registry = power_monitor.get_registry()
225        if 'bits_service' not in registry:
226            raise ValueError('No bits_service binary has been defined in the '
227                             'global registry.')
228        if 'bits_client' not in registry:
229            raise ValueError('No bits_client binary has been defined in the '
230                             'global registry.')
231
232        bits_service_binary = _get_single_file(registry, 'bits_service')
233        bits_client_binary = _get_single_file(registry, 'bits_client')
234        lvpm_monsoon_bin = _get_single_file(registry, 'lvpm_monsoon')
235        hvpm_monsoon_bin = _get_single_file(registry, 'hvpm_monsoon')
236        kibble_bin = _get_single_file(registry, 'kibble_bin')
237        kibble_board_file = _get_single_file(registry, 'kibble_board_file')
238        vm_file = _get_single_file(registry, 'vm_file')
239        config = bsc.BitsServiceConfig(self.config,
240                                       lvpm_monsoon_bin=lvpm_monsoon_bin,
241                                       hvpm_monsoon_bin=hvpm_monsoon_bin,
242                                       kibble_bin=kibble_bin,
243                                       kibble_board_file=kibble_board_file,
244                                       virtual_metrics_file=vm_file)
245        output_log = os.path.join(
246            context.get_current_context().get_full_output_path(),
247            'bits_service_out_%s.txt' % self.index)
248        service_name = 'bits_config_%s' % self.index
249
250        self._active_collection = None
251        self._collections_counter = 0
252        self._service = bits_service.BitsService(config,
253                                                 bits_service_binary,
254                                                 output_log,
255                                                 name=service_name,
256                                                 timeout=3600 * 24)
257        self._service.start()
258        self._client = bits_client.BitsClient(bits_client_binary,
259                                              self._service,
260                                              config)
261        # this call makes sure that the client can interact with the server.
262        devices = self._client.list_devices()
263        logging.debug(devices)
264
265    def disconnect_usb(self, *_, **__):
266        self._client.disconnect_usb()
267
268    def connect_usb(self, *_, **__):
269        self._client.connect_usb()
270
271    def measure(self, *_, measurement_args=None,
272                measurement_name=None, monsoon_output_path=None,
273                **__):
274        """Blocking function that measures power through bits for the specified
275        duration. Results need to be consulted through other methods such as
276        get_metrics or post processing files like the ones
277        generated at monsoon_output_path after calling `release_resources`.
278
279        Args:
280            measurement_args: A dictionary with the following structure:
281                {
282                   'duration': <seconds to measure for>
283                   'hz': <samples per second>
284                   'measure_after_seconds': <sleep time before measurement>
285                }
286                The actual number of samples per second is limited by the
287                bits configuration. The value of hz is defaulted to 1000.
288            measurement_name: A name to give to the measurement (which is also
289                used as the Bits collection name. Bits collection names (and
290                therefore measurement names) need to be unique within the
291                context of a Bits object.
292            monsoon_output_path: If provided this path will be used to generate
293                a monsoon like formatted file at the release_resources step.
294        """
295        if measurement_args is None:
296            raise ValueError('measurement_args can not be left undefined')
297
298        duration = measurement_args.get('duration')
299        if duration is None:
300            raise ValueError(
301                'duration can not be left undefined within measurement_args')
302
303        hz = measurement_args.get('hz', 1000)
304
305        # Delay the start of the measurement if an offset is required
306        measure_after_seconds = measurement_args.get('measure_after_seconds')
307        if measure_after_seconds:
308            time.sleep(measure_after_seconds)
309
310        if self._active_collection:
311            raise BitsError(
312                'Attempted to start a collection while there is still an '
313                'active one. Active collection: %s',
314                self._active_collection.name)
315
316        self._collections_counter = self._collections_counter + 1
317        # The name gets a random 8 characters salt suffix because the Bits
318        # client has a bug where files with the same name are considered to be
319        # the same collection and it won't load two files with the same name.
320        # b/153170987 b/153944171
321        if not measurement_name:
322            measurement_name = 'bits_collection_%s_%s' % (
323                str(self._collections_counter), str(uuid.uuid4())[0:8])
324
325        self._active_collection = _BitsCollection(measurement_name,
326                                                  monsoon_output_path)
327        self._client.start_collection(self._active_collection.name,
328                                      default_sampling_rate=hz)
329        time.sleep(duration)
330
331    def get_metrics(self, *_, timestamps=None, **__):
332        """Gets metrics for the segments delimited by the timestamps dictionary.
333
334        Must be called before releasing resources, otherwise it will fail adding
335        markers to the collection.
336
337        Args:
338            timestamps: A dictionary of the shape:
339                {
340                    'segment_name': {
341                        'start' : <milliseconds_since_epoch> or <datetime>
342                        'end': <milliseconds_since_epoch> or <datetime>
343                    }
344                    'another_segment': {
345                        'start' : <milliseconds_since_epoch> or <datetime>
346                        'end': <milliseconds_since_epoch> or <datetime>
347                    }
348                }
349        Returns:
350            A dictionary of the shape:
351                {
352                    'segment_name': <list of power_metrics.Metric>
353                    'another_segment': <list of power_metrics.Metric>
354                }
355        """
356        if timestamps is None:
357            raise ValueError('timestamps dictionary can not be left undefined')
358
359        metrics = {}
360
361        for segment_name, times in timestamps.items():
362            if 'start' not in times or 'end' not in times:
363                continue
364
365            start = times['start']
366            end = times['end']
367
368            # bits accepts nanoseconds only, but since this interface needs to
369            # backwards compatible with monsoon which works with milliseconds we
370            # require to do a conversion from milliseconds to nanoseconds.
371            # The preferred way for new calls to this function should be using
372            # datetime instead which is unambiguous
373            if isinstance(start, (int, float)):
374                start = start * 1e6
375            if isinstance(end, (int, float)):
376                end = end * 1e6
377
378            raw_metrics = self._client.get_metrics(self._active_collection.name,
379                                                   start=start, end=end)
380            self._add_marker(start, 'start - %s' % segment_name)
381            self._add_marker(end, 'end - %s' % segment_name)
382            metrics[segment_name] = _raw_data_to_metrics(raw_metrics)
383        return metrics
384
385    def _add_marker(self, timestamp, marker_text):
386        if not self._active_collection:
387            raise BitsError(
388                'markers can not be added without an active collection')
389        self._active_collection.add_marker(timestamp, marker_text)
390
391    def release_resources(self):
392        """Performs all the cleanup and export tasks.
393
394        In the way that Bits' is interfaced several tasks can not be performed
395        while a collection is still active (like exporting the data) and others
396        can only take place while the collection is still active (like adding
397        markers to a collection).
398
399        To workaround this unique workflow, the collections that are started
400        with the 'measure' method are not really stopped after the method
401        is unblocked, it is only stopped after this method is called.
402
403        All the export files (.7z.bits and monsoon-formatted file) are also
404        generated in this method.
405        """
406        if not self._active_collection:
407            raise BitsError(
408                'Attempted to stop a collection without starting one')
409        self._client.add_markers(self._active_collection.name,
410                                 self._active_collection.markers_buffer)
411        self._client.stop_collection(self._active_collection.name)
412
413        export_file = os.path.join(
414            context.get_current_context().get_full_output_path(),
415            '%s.7z.bits' % self._active_collection.name)
416        self._client.export(self._active_collection.name, export_file)
417        if self._active_collection.monsoon_output_path:
418            self._attempt_monsoon_format()
419        self._active_collection = None
420
421    def _attempt_monsoon_format(self):
422        """Attempts to create a monsoon-formatted file.
423
424        In the case where there is not enough information to retrieve a
425        monsoon-like file, this function will do nothing.
426        """
427        available_channels = self._client.list_channels(
428            self._active_collection.name)
429        milli_amps_channel = None
430
431        for channel in available_channels:
432            if channel.endswith(self._root_rail):
433                milli_amps_channel = self._root_rail
434                break
435
436        if milli_amps_channel is None:
437            logging.debug('No monsoon equivalent channels were found when '
438                          'attempting to recreate monsoon file format. '
439                          'Available channels were: %s',
440                          str(available_channels))
441            return
442
443        logging.debug('Recreating monsoon file format from channel: %s',
444                      milli_amps_channel)
445        self._client.export_as_monsoon_format(
446            self._active_collection.monsoon_output_path,
447            self._active_collection.name,
448            milli_amps_channel)
449
450    def get_waveform(self, file_path=None):
451        """Parses a file generated in release_resources.
452
453        Args:
454            file_path: Path to a waveform file.
455
456        Returns:
457            A list of tuples in which the first element is a timestamp and the
458            second element is the sampled current at that time.
459        """
460        if file_path is None:
461            raise ValueError('file_path can not be None')
462
463        return list(power_metrics.import_raw_data(file_path))
464
465    def teardown(self):
466        if self._service is None:
467            return
468
469        if self._service.service_state == bits_service.BitsServiceStates.STARTED:
470            self._service.stop()
471