1#!/usr/bin/env python3
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"""
17Class for HTTP control of Mini-Circuits RCDAT series attenuators
18
19This class provides a wrapper to the MC-RCDAT attenuator modules for purposes
20of simplifying and abstracting control down to the basic necessities. It is
21not the intention of the module to expose all functionality, but to allow
22interchangeable HW to be used.
23
24See http://www.minicircuits.com/softwaredownload/Prog_Manual-6-Programmable_Attenuator.pdf
25"""
26
27import urllib
28from acts.controllers import attenuator
29
30
31class AttenuatorInstrument(attenuator.AttenuatorInstrument):
32    """A specific HTTP-controlled implementation of AttenuatorInstrument for
33    Mini-Circuits RC-DAT attenuators.
34
35    With the exception of HTTP-specific commands, all functionality is defined
36    by the AttenuatorInstrument class.
37    """
38
39    def __init__(self, num_atten=1):
40        super(AttenuatorInstrument, self).__init__(num_atten)
41        self._ip_address = None
42        self._port = None
43        self._timeout = None
44        self.address = None
45
46    def open(self, host, port=80, timeout=2):
47        """Initializes the AttenuatorInstrument and queries basic information.
48
49        Args:
50            host: A valid hostname (IP address or DNS-resolvable name) to an
51            MC-DAT attenuator instrument.
52            port: An optional port number (defaults to http default 80)
53            timeout: An optional timeout for http requests
54        """
55        self._ip_address = host
56        self._port = port
57        self._timeout = timeout
58        self.address = host
59
60        att_req = urllib.request.urlopen('http://{}:{}/MN?'.format(
61            self._ip_address, self._port))
62        config_str = att_req.read().decode('utf-8').strip()
63        if not config_str.startswith('MN='):
64            raise attenuator.InvalidDataError(
65                'Attenuator returned invalid data. Attenuator returned: {}'.
66                format(config_str))
67
68        config_str = config_str[len('MN='):]
69        self.properties = dict(
70            zip(['model', 'max_freq', 'max_atten'], config_str.split('-', 2)))
71        self.max_atten = float(self.properties['max_atten'])
72
73    def is_open(self):
74        """Returns True if the AttenuatorInstrument has an open connection.
75
76        Since this controller is based on HTTP requests, there is no connection
77        required and the attenuator is always ready to accept requests.
78        """
79        return True
80
81    def close(self):
82        """Closes the connection to the attenuator.
83
84        Since this controller is based on HTTP requests, there is no connection
85        teardowns required.
86        """
87
88    def set_atten(self, idx, value, strict=True, retry=False, **_):
89        """This function sets the attenuation of an attenuator given its index
90        in the instrument.
91
92        Args:
93            idx: A zero-based index that identifies a particular attenuator in
94                an instrument. For instruments that only have one channel, this
95                is ignored by the device.
96            value: A floating point value for nominal attenuation to be set.
97            strict: if True, function raises an error when given out of
98                bounds attenuation values, if false, the function sets out of
99                bounds values to 0 or max_atten.
100            retry: if True, command will be retried if possible
101
102        Raises:
103            InvalidDataError if the attenuator does not respond with the
104            expected output.
105        """
106        if not (0 <= idx < self.num_atten):
107            raise IndexError('Attenuator index out of range!', self.num_atten,
108                             idx)
109
110        if value > self.max_atten and strict:
111            raise ValueError('Attenuator value out of range!', self.max_atten,
112                             value)
113        # The actual device uses one-based index for channel numbers.
114        adjusted_value = min(max(0, value), self.max_atten)
115        att_req = urllib.request.urlopen(
116            'http://{}:{}/CHAN:{}:SETATT:{}'.format(self._ip_address,
117                                                    self._port, idx + 1,
118                                                    adjusted_value),
119            timeout=self._timeout)
120        att_resp = att_req.read().decode('utf-8').strip()
121        if att_resp != '1':
122            if retry:
123                self.set_atten(idx, value, strict, retry=False)
124            else:
125                raise attenuator.InvalidDataError(
126                    'Attenuator returned invalid data. Attenuator returned: {}'
127                    .format(att_resp))
128
129    def get_atten(self, idx, retry=False, **_):
130        """Returns the current attenuation of the attenuator at the given index.
131
132        Args:
133            idx: The index of the attenuator.
134            retry: if True, command will be retried if possible
135
136        Raises:
137            InvalidDataError if the attenuator does not respond with the
138            expected outpu
139
140        Returns:
141            the current attenuation value as a float
142        """
143        if not (0 <= idx < self.num_atten):
144            raise IndexError('Attenuator index out of range!', self.num_atten,
145                             idx)
146        att_req = urllib.request.urlopen(
147            'http://{}:{}/CHAN:{}:ATT?'.format(self._ip_address, self.port, idx + 1),
148            timeout=self._timeout)
149        att_resp = att_req.read().decode('utf-8').strip()
150        try:
151            atten_val = float(att_resp)
152        except:
153            if retry:
154                self.get_atten(idx, retry=False)
155            else:
156                raise attenuator.InvalidDataError(
157                    'Attenuator returned invalid data. Attenuator returned: {}'
158                    .format(att_resp))
159        return atten_val
160