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 pass 185 186 187class InvalidOperationError(AttenuatorError): 188 """Raised when the attenuator's state does not allow the given operation. 189 190 Certain methods may only be accessed when the instance upon which they are 191 invoked is in a certain state. This indicates that the object is not in the 192 correct state for a method to be called. 193 """ 194 pass 195 196 197class AttenuatorInstrument(object): 198 """Defines the primitive behavior of all attenuator instruments. 199 200 The AttenuatorInstrument class is designed to provide a simple low-level 201 interface for accessing any step attenuator instrument comprised of one or 202 more attenuators and a controller. All AttenuatorInstruments should override 203 all the methods below and call AttenuatorInstrument.__init__ in their 204 constructors. Outside of setup/teardown, devices should be accessed via 205 this generic "interface". 206 """ 207 model = None 208 INVALID_MAX_ATTEN = 999.9 209 210 def __init__(self, num_atten=0): 211 """This is the Constructor for Attenuator Instrument. 212 213 Args: 214 num_atten: The number of attenuators contained within the 215 instrument. In some instances setting this number to zero will 216 allow the driver to auto-determine the number of attenuators; 217 however, this behavior is not guaranteed. 218 219 Raises: 220 NotImplementedError if initialization is called from this class. 221 """ 222 223 if type(self) is AttenuatorInstrument: 224 raise NotImplementedError( 225 'Base class should not be instantiated directly!') 226 227 self.num_atten = num_atten 228 self.max_atten = AttenuatorInstrument.INVALID_MAX_ATTEN 229 self.properties = None 230 231 def set_atten(self, idx, value, strict=True): 232 """Sets the attenuation given its index in the instrument. 233 234 Args: 235 idx: A zero based index used to identify a particular attenuator in 236 an instrument. 237 value: a floating point value for nominal attenuation to be set. 238 strict: if True, function raises an error when given out of 239 bounds attenuation values, if false, the function sets out of 240 bounds values to 0 or max_atten. 241 """ 242 raise NotImplementedError('Base class should not be called directly!') 243 244 def get_atten(self, idx): 245 """Returns the current attenuation of the attenuator at index idx. 246 247 Args: 248 idx: A zero based index used to identify a particular attenuator in 249 an instrument. 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 def __init__(self, instrument, idx=0, offset=0): 266 """This is the constructor for Attenuator 267 268 Args: 269 instrument: Reference to an AttenuatorInstrument on which the 270 Attenuator resides 271 idx: This zero-based index is the identifier for a particular 272 attenuator in an instrument. 273 offset: A power offset value for the attenuator to be used when 274 performing future operations. This could be used for either 275 calibration or to allow group operations with offsets between 276 various attenuators. 277 278 Raises: 279 TypeError if an invalid AttenuatorInstrument is passed in. 280 IndexError if the index is out of range. 281 """ 282 if not isinstance(instrument, AttenuatorInstrument): 283 raise TypeError('Must provide an Attenuator Instrument Ref') 284 self.model = instrument.model 285 self.instrument = instrument 286 self.idx = idx 287 self.offset = offset 288 289 if self.idx >= instrument.num_atten: 290 raise IndexError( 291 'Attenuator index out of range for attenuator instrument') 292 293 def set_atten(self, value, strict=True): 294 """Sets the attenuation. 295 296 Args: 297 value: A floating point value for nominal attenuation to be set. 298 strict: if True, function raises an error when given out of 299 bounds attenuation values, if false, the function sets out of 300 bounds values to 0 or max_atten. 301 302 Raises: 303 ValueError if value + offset is greater than the maximum value. 304 """ 305 if value + self.offset > self.instrument.max_atten and strict: 306 raise ValueError( 307 'Attenuator Value+Offset greater than Max Attenuation!') 308 309 self.instrument.set_atten(self.idx, value + self.offset, strict) 310 311 def get_atten(self): 312 """Returns the attenuation as a float, normalized by the offset.""" 313 return self.instrument.get_atten(self.idx) - self.offset 314 315 def get_max_atten(self): 316 """Returns the max attenuation as a float, normalized by the offset.""" 317 if self.instrument.max_atten == AttenuatorInstrument.INVALID_MAX_ATTEN: 318 raise ValueError('Invalid Max Attenuator Value') 319 320 return self.instrument.max_atten - self.offset 321 322 323class AttenuatorGroup(object): 324 """An abstraction for groups of attenuators that will share behavior. 325 326 Attenuator groups are intended to further facilitate abstraction of testing 327 functions from the physical objects underlying them. By adding attenuators 328 to a group, it is possible to operate on functional groups that can be 329 thought of in a common manner in the test. This class is intended to provide 330 convenience to the user and avoid re-implementation of helper functions and 331 small loops scattered throughout user code. 332 """ 333 def __init__(self, name=''): 334 """This constructor for AttenuatorGroup 335 336 Args: 337 name: An optional parameter intended to further facilitate the 338 passing of easily tracked groups of attenuators throughout code. 339 It is left to the user to use the name in a way that meets their 340 needs. 341 """ 342 self.name = name 343 self.attens = [] 344 self._value = 0 345 346 def add_from_instrument(self, instrument, indices): 347 """Adds an AttenuatorInstrument to the group. 348 349 This function will create Attenuator objects for all of the indices 350 passed in and add them to the group. 351 352 Args: 353 instrument: the AttenuatorInstrument to pull attenuators from. 354 indices: The index or indices to add to the group. Either a 355 range, a list, or a single integer. 356 357 Raises 358 ------ 359 TypeError 360 Requires a valid AttenuatorInstrument to be passed in. 361 """ 362 if not instrument or not isinstance(instrument, AttenuatorInstrument): 363 raise TypeError('Must provide an Attenuator Instrument Ref') 364 365 if type(indices) is range or type(indices) is list: 366 for i in indices: 367 self.attens.append(Attenuator(instrument, i)) 368 elif type(indices) is int: 369 self.attens.append(Attenuator(instrument, indices)) 370 371 def add(self, attenuator): 372 """Adds an already constructed Attenuator object to this group. 373 374 Args: 375 attenuator: An Attenuator object. 376 377 Raises: 378 TypeError if the attenuator parameter is not an Attenuator. 379 """ 380 if not isinstance(attenuator, Attenuator): 381 raise TypeError('Must provide an Attenuator') 382 383 self.attens.append(attenuator) 384 385 def synchronize(self): 386 """Sets all grouped attenuators to the group's attenuation value.""" 387 self.set_atten(self._value) 388 389 def is_synchronized(self): 390 """Returns true if all attenuators have the synchronized value.""" 391 for att in self.attens: 392 if att.get_atten() != self._value: 393 return False 394 return True 395 396 def set_atten(self, value): 397 """Sets the attenuation value of all attenuators in the group. 398 399 Args: 400 value: A floating point value for nominal attenuation to be set. 401 """ 402 value = float(value) 403 for att in self.attens: 404 att.set_atten(value) 405 self._value = value 406 407 def get_atten(self): 408 """Returns the current attenuation setting of AttenuatorGroup.""" 409 return float(self._value) 410