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