1#!/usr/bin/env python3.4 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import importlib 18import logging 19 20from acts.keys import Config 21from acts.libs.proc import job 22 23MOBLY_CONTROLLER_CONFIG_NAME = 'Attenuator' 24ACTS_CONTROLLER_REFERENCE_NAME = 'attenuators' 25_ATTENUATOR_OPEN_RETRIES = 3 26 27 28def create(configs): 29 objs = [] 30 for c in configs: 31 attn_model = c['Model'] 32 # Default to telnet. 33 protocol = c.get('Protocol', 'telnet') 34 module_name = 'acts.controllers.attenuator_lib.%s.%s' % (attn_model, 35 protocol) 36 module = importlib.import_module(module_name) 37 inst_cnt = c['InstrumentCount'] 38 attn_inst = module.AttenuatorInstrument(inst_cnt) 39 attn_inst.model = attn_model 40 41 ip_address = c[Config.key_address.value] 42 port = c[Config.key_port.value] 43 44 for attempt_number in range(1, _ATTENUATOR_OPEN_RETRIES + 1): 45 try: 46 attn_inst.open(ip_address, port) 47 except Exception as e: 48 logging.error('Attempt %s to open connection to attenuator ' 49 'failed: %s' % (attempt_number, e)) 50 if attempt_number == _ATTENUATOR_OPEN_RETRIES: 51 ping_output = job.run('ping %s -c 1 -w 1' % ip_address, 52 ignore_status=True) 53 if ping_output.exit_status == 1: 54 logging.error('Unable to ping attenuator at %s' % 55 ip_address) 56 else: 57 logging.error('Able to ping attenuator at %s' % 58 ip_address) 59 job.run('echo "q" | telnet %s %s' % (ip_address, port), 60 ignore_status=True) 61 raise 62 for i in range(inst_cnt): 63 attn = Attenuator(attn_inst, idx=i) 64 if 'Paths' in c: 65 try: 66 setattr(attn, 'path', c['Paths'][i]) 67 except IndexError: 68 logging.error('No path specified for attenuator %d.', i) 69 raise 70 objs.append(attn) 71 return objs 72 73 74def get_info(attenuators): 75 """Get information on a list of Attenuator objects. 76 77 Args: 78 attenuators: A list of Attenuator objects. 79 80 Returns: 81 A list of dict, each representing info for Attenuator objects. 82 """ 83 device_info = [] 84 for attenuator in attenuators: 85 info = { 86 "Address": attenuator.instrument.address, 87 "Attenuator_Port": attenuator.idx 88 } 89 device_info.append(info) 90 return device_info 91 92 93def destroy(objs): 94 for attn in objs: 95 attn.instrument.close() 96 97 98def get_attenuators_for_device(device_attenuator_configs, attenuators, 99 attenuator_key): 100 """Gets the list of attenuators associated to a specified device and builds 101 a list of the attenuator objects associated to the ip address in the 102 device's section of the ACTS config and the Attenuator's IP address. In the 103 example below the access point object has an attenuator dictionary with 104 IP address associated to an attenuator object. The address is the only 105 mandatory field and the 'attenuator_ports_wifi_2g' and 106 'attenuator_ports_wifi_5g' are the attenuator_key specified above. These 107 can be anything and is sent in as a parameter to this function. The numbers 108 in the list are ports that are in the attenuator object. Below is an 109 standard Access_Point object and the link to a standard Attenuator object. 110 Notice the link is the IP address, which is why the IP address is mandatory. 111 112 "AccessPoint": [ 113 { 114 "ssh_config": { 115 "user": "root", 116 "host": "192.168.42.210" 117 }, 118 "Attenuator": [ 119 { 120 "Address": "192.168.42.200", 121 "attenuator_ports_wifi_2g": [ 122 0, 123 1, 124 3 125 ], 126 "attenuator_ports_wifi_5g": [ 127 0, 128 1 129 ] 130 } 131 ] 132 } 133 ], 134 "Attenuator": [ 135 { 136 "Model": "minicircuits", 137 "InstrumentCount": 4, 138 "Address": "192.168.42.200", 139 "Port": 23 140 } 141 ] 142 Args: 143 device_attenuator_configs: A list of attenuators config information in 144 the acts config that are associated a particular device. 145 attenuators: A list of all of the available attenuators objects 146 in the testbed. 147 attenuator_key: A string that is the key to search in the device's 148 configuration. 149 150 Returns: 151 A list of attenuator objects for the specified device and the key in 152 that device's config. 153 """ 154 attenuator_list = [] 155 for device_attenuator_config in device_attenuator_configs: 156 for attenuator_port in device_attenuator_config[attenuator_key]: 157 for attenuator in attenuators: 158 if (attenuator.instrument.address == 159 device_attenuator_config['Address'] 160 and attenuator.idx is attenuator_port): 161 attenuator_list.append(attenuator) 162 return attenuator_list 163 164 165"""Classes for accessing, managing, and manipulating attenuators. 166 167Users will instantiate a specific child class, but almost all operation should 168be performed on the methods and data members defined here in the base classes 169or the wrapper classes. 170""" 171 172 173class AttenuatorError(Exception): 174 """Base class for all errors generated by Attenuator-related modules.""" 175 176 177class InvalidDataError(AttenuatorError): 178 """"Raised when an unexpected result is seen on the transport layer. 179 180 When this exception is seen, closing an re-opening the link to the 181 attenuator instrument is probably necessary. Something has gone wrong in 182 the transport. 183 """ 184 185 186class InvalidOperationError(AttenuatorError): 187 """Raised when the attenuator's state does not allow the given operation. 188 189 Certain methods may only be accessed when the instance upon which they are 190 invoked is in a certain state. This indicates that the object is not in the 191 correct state for a method to be called. 192 """ 193 194 195class AttenuatorInstrument(object): 196 """Defines the primitive behavior of all attenuator instruments. 197 198 The AttenuatorInstrument class is designed to provide a simple low-level 199 interface for accessing any step attenuator instrument comprised of one or 200 more attenuators and a controller. All AttenuatorInstruments should override 201 all the methods below and call AttenuatorInstrument.__init__ in their 202 constructors. Outside of setup/teardown, devices should be accessed via 203 this generic "interface". 204 """ 205 model = None 206 INVALID_MAX_ATTEN = 999.9 207 208 def __init__(self, num_atten=0): 209 """This is the Constructor for Attenuator Instrument. 210 211 Args: 212 num_atten: The number of attenuators contained within the 213 instrument. In some instances setting this number to zero will 214 allow the driver to auto-determine the number of attenuators; 215 however, this behavior is not guaranteed. 216 217 Raises: 218 NotImplementedError if initialization is called from this class. 219 """ 220 221 if type(self) is AttenuatorInstrument: 222 raise NotImplementedError( 223 'Base class should not be instantiated directly!') 224 225 self.num_atten = num_atten 226 self.max_atten = AttenuatorInstrument.INVALID_MAX_ATTEN 227 self.properties = None 228 229 def set_atten(self, idx, value, strict=True, retry=False): 230 """Sets the attenuation given its index in the instrument. 231 232 Args: 233 idx: A zero based index used to identify a particular attenuator in 234 an instrument. 235 value: a floating point value for nominal attenuation to be set. 236 strict: if True, function raises an error when given out of 237 bounds attenuation values, if false, the function sets out of 238 bounds values to 0 or max_atten. 239 retry: if True, command will be retried if possible 240 """ 241 raise NotImplementedError('Base class should not be called directly!') 242 243 def get_atten(self, idx, retry=False): 244 """Returns the current attenuation of the attenuator at index idx. 245 246 Args: 247 idx: A zero based index used to identify a particular attenuator in 248 an instrument. 249 retry: if True, command will be retried if possible 250 251 Returns: 252 The current attenuation value as a floating point value 253 """ 254 raise NotImplementedError('Base class should not be called directly!') 255 256 257class Attenuator(object): 258 """An object representing a single attenuator in a remote instrument. 259 260 A user wishing to abstract the mapping of attenuators to physical 261 instruments should use this class, which provides an object that abstracts 262 the physical implementation and allows the user to think only of attenuators 263 regardless of their location. 264 """ 265 266 def __init__(self, instrument, idx=0, offset=0): 267 """This is the constructor for Attenuator 268 269 Args: 270 instrument: Reference to an AttenuatorInstrument on which the 271 Attenuator resides 272 idx: This zero-based index is the identifier for a particular 273 attenuator in an instrument. 274 offset: A power offset value for the attenuator to be used when 275 performing future operations. This could be used for either 276 calibration or to allow group operations with offsets between 277 various attenuators. 278 279 Raises: 280 TypeError if an invalid AttenuatorInstrument is passed in. 281 IndexError if the index is out of range. 282 """ 283 if not isinstance(instrument, AttenuatorInstrument): 284 raise TypeError('Must provide an Attenuator Instrument Ref') 285 self.model = instrument.model 286 self.instrument = instrument 287 self.idx = idx 288 self.offset = offset 289 290 if self.idx >= instrument.num_atten: 291 raise IndexError( 292 'Attenuator index out of range for attenuator instrument') 293 294 def set_atten(self, value, strict=True, retry=False): 295 """Sets the attenuation. 296 297 Args: 298 value: A floating point value for nominal attenuation to be set. 299 strict: if True, function raises an error when given out of 300 bounds attenuation values, if false, the function sets out of 301 bounds values to 0 or max_atten. 302 retry: if True, command will be retried if possible 303 304 Raises: 305 ValueError if value + offset is greater than the maximum value. 306 """ 307 if value + self.offset > self.instrument.max_atten and strict: 308 raise ValueError( 309 'Attenuator Value+Offset greater than Max Attenuation!') 310 311 self.instrument.set_atten(self.idx, 312 value + self.offset, 313 strict=strict, 314 retry=retry) 315 316 def get_atten(self, retry=False): 317 """Returns the attenuation as a float, normalized by the offset.""" 318 return self.instrument.get_atten(self.idx, retry) - self.offset 319 320 def get_max_atten(self): 321 """Returns the max attenuation as a float, normalized by the offset.""" 322 if self.instrument.max_atten == AttenuatorInstrument.INVALID_MAX_ATTEN: 323 raise ValueError('Invalid Max Attenuator Value') 324 325 return self.instrument.max_atten - self.offset 326 327 328class AttenuatorGroup(object): 329 """An abstraction for groups of attenuators that will share behavior. 330 331 Attenuator groups are intended to further facilitate abstraction of testing 332 functions from the physical objects underlying them. By adding attenuators 333 to a group, it is possible to operate on functional groups that can be 334 thought of in a common manner in the test. This class is intended to provide 335 convenience to the user and avoid re-implementation of helper functions and 336 small loops scattered throughout user code. 337 """ 338 339 def __init__(self, name=''): 340 """This constructor for AttenuatorGroup 341 342 Args: 343 name: An optional parameter intended to further facilitate the 344 passing of easily tracked groups of attenuators throughout code. 345 It is left to the user to use the name in a way that meets their 346 needs. 347 """ 348 self.name = name 349 self.attens = [] 350 self._value = 0 351 352 def add_from_instrument(self, instrument, indices): 353 """Adds an AttenuatorInstrument to the group. 354 355 This function will create Attenuator objects for all of the indices 356 passed in and add them to the group. 357 358 Args: 359 instrument: the AttenuatorInstrument to pull attenuators from. 360 indices: The index or indices to add to the group. Either a 361 range, a list, or a single integer. 362 363 Raises 364 ------ 365 TypeError 366 Requires a valid AttenuatorInstrument to be passed in. 367 """ 368 if not instrument or not isinstance(instrument, AttenuatorInstrument): 369 raise TypeError('Must provide an Attenuator Instrument Ref') 370 371 if type(indices) is range or type(indices) is list: 372 for i in indices: 373 self.attens.append(Attenuator(instrument, i)) 374 elif type(indices) is int: 375 self.attens.append(Attenuator(instrument, indices)) 376 377 def add(self, attenuator): 378 """Adds an already constructed Attenuator object to this group. 379 380 Args: 381 attenuator: An Attenuator object. 382 383 Raises: 384 TypeError if the attenuator parameter is not an Attenuator. 385 """ 386 if not isinstance(attenuator, Attenuator): 387 raise TypeError('Must provide an Attenuator') 388 389 self.attens.append(attenuator) 390 391 def synchronize(self): 392 """Sets all grouped attenuators to the group's attenuation value.""" 393 self.set_atten(self._value) 394 395 def is_synchronized(self): 396 """Returns true if all attenuators have the synchronized value.""" 397 for att in self.attens: 398 if att.get_atten() != self._value: 399 return False 400 return True 401 402 def set_atten(self, value): 403 """Sets the attenuation value of all attenuators in the group. 404 405 Args: 406 value: A floating point value for nominal attenuation to be set. 407 """ 408 value = float(value) 409 for att in self.attens: 410 att.set_atten(value) 411 self._value = value 412 413 def get_atten(self): 414 """Returns the current attenuation setting of AttenuatorGroup.""" 415 return float(self._value) 416