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, retry=False): 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 retry: if True, command will be retried if possible 242 """ 243 raise NotImplementedError('Base class should not be called directly!') 244 245 def get_atten(self, idx, retry=False): 246 """Returns the current attenuation of the attenuator at index idx. 247 248 Args: 249 idx: A zero based index used to identify a particular attenuator in 250 an instrument. 251 retry: if True, command will be retried if possible 252 253 Returns: 254 The current attenuation value as a floating point value 255 """ 256 raise NotImplementedError('Base class should not be called directly!') 257 258 259class Attenuator(object): 260 """An object representing a single attenuator in a remote instrument. 261 262 A user wishing to abstract the mapping of attenuators to physical 263 instruments should use this class, which provides an object that abstracts 264 the physical implementation and allows the user to think only of attenuators 265 regardless of their location. 266 """ 267 268 def __init__(self, instrument, idx=0, offset=0): 269 """This is the constructor for Attenuator 270 271 Args: 272 instrument: Reference to an AttenuatorInstrument on which the 273 Attenuator resides 274 idx: This zero-based index is the identifier for a particular 275 attenuator in an instrument. 276 offset: A power offset value for the attenuator to be used when 277 performing future operations. This could be used for either 278 calibration or to allow group operations with offsets between 279 various attenuators. 280 281 Raises: 282 TypeError if an invalid AttenuatorInstrument is passed in. 283 IndexError if the index is out of range. 284 """ 285 if not isinstance(instrument, AttenuatorInstrument): 286 raise TypeError('Must provide an Attenuator Instrument Ref') 287 self.model = instrument.model 288 self.instrument = instrument 289 self.idx = idx 290 self.offset = offset 291 292 if self.idx >= instrument.num_atten: 293 raise IndexError( 294 'Attenuator index out of range for attenuator instrument') 295 296 def set_atten(self, value, strict=True, retry=False): 297 """Sets the attenuation. 298 299 Args: 300 value: A floating point value for nominal attenuation to be set. 301 strict: if True, function raises an error when given out of 302 bounds attenuation values, if false, the function sets out of 303 bounds values to 0 or max_atten. 304 retry: if True, command will be retried if possible 305 306 Raises: 307 ValueError if value + offset is greater than the maximum value. 308 """ 309 if value + self.offset > self.instrument.max_atten and strict: 310 raise ValueError( 311 'Attenuator Value+Offset greater than Max Attenuation!') 312 313 self.instrument.set_atten(self.idx, 314 value + self.offset, 315 strict=strict, 316 retry=retry) 317 318 def get_atten(self, retry=False): 319 """Returns the attenuation as a float, normalized by the offset.""" 320 return self.instrument.get_atten(self.idx, retry) - self.offset 321 322 def get_max_atten(self): 323 """Returns the max attenuation as a float, normalized by the offset.""" 324 if self.instrument.max_atten == AttenuatorInstrument.INVALID_MAX_ATTEN: 325 raise ValueError('Invalid Max Attenuator Value') 326 327 return self.instrument.max_atten - self.offset 328 329 330class AttenuatorGroup(object): 331 """An abstraction for groups of attenuators that will share behavior. 332 333 Attenuator groups are intended to further facilitate abstraction of testing 334 functions from the physical objects underlying them. By adding attenuators 335 to a group, it is possible to operate on functional groups that can be 336 thought of in a common manner in the test. This class is intended to provide 337 convenience to the user and avoid re-implementation of helper functions and 338 small loops scattered throughout user code. 339 """ 340 341 def __init__(self, name=''): 342 """This constructor for AttenuatorGroup 343 344 Args: 345 name: An optional parameter intended to further facilitate the 346 passing of easily tracked groups of attenuators throughout code. 347 It is left to the user to use the name in a way that meets their 348 needs. 349 """ 350 self.name = name 351 self.attens = [] 352 self._value = 0 353 354 def add_from_instrument(self, instrument, indices): 355 """Adds an AttenuatorInstrument to the group. 356 357 This function will create Attenuator objects for all of the indices 358 passed in and add them to the group. 359 360 Args: 361 instrument: the AttenuatorInstrument to pull attenuators from. 362 indices: The index or indices to add to the group. Either a 363 range, a list, or a single integer. 364 365 Raises 366 ------ 367 TypeError 368 Requires a valid AttenuatorInstrument to be passed in. 369 """ 370 if not instrument or not isinstance(instrument, AttenuatorInstrument): 371 raise TypeError('Must provide an Attenuator Instrument Ref') 372 373 if type(indices) is range or type(indices) is list: 374 for i in indices: 375 self.attens.append(Attenuator(instrument, i)) 376 elif type(indices) is int: 377 self.attens.append(Attenuator(instrument, indices)) 378 379 def add(self, attenuator): 380 """Adds an already constructed Attenuator object to this group. 381 382 Args: 383 attenuator: An Attenuator object. 384 385 Raises: 386 TypeError if the attenuator parameter is not an Attenuator. 387 """ 388 if not isinstance(attenuator, Attenuator): 389 raise TypeError('Must provide an Attenuator') 390 391 self.attens.append(attenuator) 392 393 def synchronize(self): 394 """Sets all grouped attenuators to the group's attenuation value.""" 395 self.set_atten(self._value) 396 397 def is_synchronized(self): 398 """Returns true if all attenuators have the synchronized value.""" 399 for att in self.attens: 400 if att.get_atten() != self._value: 401 return False 402 return True 403 404 def set_atten(self, value): 405 """Sets the attenuation value of all attenuators in the group. 406 407 Args: 408 value: A floating point value for nominal attenuation to be set. 409 """ 410 value = float(value) 411 for att in self.attens: 412 att.set_atten(value) 413 self._value = value 414 415 def get_atten(self): 416 """Returns the current attenuation setting of AttenuatorGroup.""" 417 return float(self._value) 418