1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import os
7
8from telemetry import decorators
9from telemetry.core.platform.power_monitor import sysfs_power_monitor
10from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
11
12
13SAMPLE_RATE_HZ = 2 # The data is collected from the ds2784 fuel gauge chip
14                   # that only updates its data every 3.5s.
15FUEL_GAUGE_PATH = '/sys/class/power_supply/ds2784-fuelgauge'
16CHARGE_COUNTER = os.path.join(FUEL_GAUGE_PATH, 'charge_counter_ext')
17CURRENT = os.path.join(FUEL_GAUGE_PATH, 'current_now')
18VOLTAGE = os.path.join(FUEL_GAUGE_PATH, 'voltage_now')
19
20
21class DS2784PowerMonitor(sysfs_power_monitor.SysfsPowerMonitor):
22  def __init__(self, device, platform_backend):
23    super(DS2784PowerMonitor, self).__init__(platform_backend)
24    self._device = device
25    self._powermonitor_process_port = None
26    self._file_poller_binary = android_prebuilt_profiler_helper.GetDevicePath(
27        'file_poller')
28
29  @decorators.Cache
30  def _HasFuelGauge(self):
31    return self._device.FileExists(CHARGE_COUNTER)
32
33  def CanMonitorPower(self):
34    if not self._HasFuelGauge():
35      return False
36    if self._device.old_interface.IsDeviceCharging():
37      logging.warning('Can\'t monitor power usage since device is charging.')
38      return False
39    return True
40
41  def StartMonitoringPower(self, browser):
42    assert not self._powermonitor_process_port, (
43        'Must call StopMonitoringPower().')
44    super(DS2784PowerMonitor, self).StartMonitoringPower(browser)
45    android_prebuilt_profiler_helper.InstallOnDevice(
46        self._device, 'file_poller')
47    self._powermonitor_process_port = int(
48        self._device.RunShellCommand(
49            '%s %d %s %s %s' % (self._file_poller_binary, SAMPLE_RATE_HZ,
50                                CHARGE_COUNTER, CURRENT, VOLTAGE))[0])
51
52  def StopMonitoringPower(self):
53    assert self._powermonitor_process_port, (
54        'StartMonitoringPower() not called.')
55    try:
56      cpu_stats = super(DS2784PowerMonitor, self).StopMonitoringPower()
57      result = '\n'.join(self._device.RunShellCommand(
58          '%s %d' % (self._file_poller_binary,
59                     self._powermonitor_process_port)))
60      assert result, 'PowerMonitor produced no output'
61      return super(DS2784PowerMonitor, self).CombineResults(
62          cpu_stats, DS2784PowerMonitor.ParseSamplingOutput(result))
63    finally:
64      self._powermonitor_process_port = None
65
66  @staticmethod
67  def ParseSamplingOutput(powermonitor_output):
68    """Parse output of powermonitor command line utility.
69
70    Returns:
71        Dictionary in the format returned by StopMonitoringPower().
72    """
73    power_samples = []
74    total_energy_consumption_mwh = 0
75    def ParseSample(sample):
76      values = [float(x) for x in sample.split(' ')]
77      res = {}
78      (res['timestamp_s'],
79       res['charge_nah'],
80       res['current_ua'],
81       res['voltage_uv']) = values
82      return res
83    # The output contains a sample per line.
84    samples = map(ParseSample, powermonitor_output.split('\n')[:-1])
85    # Keep track of the last sample that found an updated reading.
86    last_updated_sample = samples[0]
87    # Compute average voltage.
88    voltage_sum_uv = 0
89    voltage_count = 0
90    for sample in samples:
91      if sample['charge_nah'] != last_updated_sample['charge_nah']:
92        charge_difference_nah = (sample['charge_nah'] -
93                                 last_updated_sample['charge_nah'])
94        # Use average voltage for the energy consumption.
95        voltage_sum_uv += sample['voltage_uv']
96        voltage_count += 1
97        average_voltage_uv = voltage_sum_uv / voltage_count
98        total_energy_consumption_mwh += (-charge_difference_nah *
99                                         average_voltage_uv / 10 ** 12)
100        last_updated_sample = sample
101        voltage_sum_uv = 0
102        voltage_count = 0
103      # Update average voltage.
104      voltage_sum_uv += sample['voltage_uv']
105      voltage_count += 1
106      # Compute energy of the sample.
107      energy_consumption_mw = (-sample['current_ua'] * sample['voltage_uv'] /
108                               10 ** 9)
109
110      power_samples.append(energy_consumption_mw)
111    # Because the data is stalled for a few seconds, compute the remaining
112    # energy consumption using the last available current reading.
113    last_sample = samples[-1]
114    remaining_time_h = (
115        last_sample['timestamp_s'] - last_updated_sample['timestamp_s']) / 3600
116    average_voltage_uv = voltage_sum_uv / voltage_count
117
118    remaining_energy_consumption_mwh = (-last_updated_sample['current_ua'] *
119                                        average_voltage_uv *
120                                        remaining_time_h  / 10 ** 9)
121    total_energy_consumption_mwh += remaining_energy_consumption_mwh
122
123    # -------- Collect and Process Data -------------
124    out_dict = {}
125    # Raw power usage samples.
126    out_dict['identifier'] = 'ds2784'
127    out_dict['power_samples_mw'] = power_samples
128    out_dict['energy_consumption_mwh'] = total_energy_consumption_mwh
129
130    return out_dict
131