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