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