1#!/usr/bin/env python3
2#
3#   Copyright 2017 - 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 logging
18import xmlrpc.client
19from subprocess import call
20
21from acts import signals
22
23MOBLY_CONTROLLER_CONFIG_NAME = "ChameleonDevice"
24ACTS_CONTROLLER_REFERENCE_NAME = "chameleon_devices"
25
26CHAMELEON_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
27CHAMELEON_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
28
29audio_bus_endpoints = {
30    'CROS_HEADPHONE': 'Cros device headphone',
31    'CROS_EXTERNAL_MICROPHONE': 'Cros device external microphone',
32    'PERIPHERAL_MICROPHONE': 'Peripheral microphone',
33    'PERIPHERAL_SPEAKER': 'Peripheral speaker',
34    'FPGA_LINEOUT': 'Chameleon FPGA line-out',
35    'FPGA_LINEIN': 'Chameleon FPGA line-in',
36    'BLUETOOTH_OUTPUT': 'Bluetooth module output',
37    'BLUETOOTH_INPUT': 'Bluetooth module input'
38}
39
40
41class ChameleonDeviceError(signals.ControllerError):
42    pass
43
44
45def create(configs):
46    if not configs:
47        raise ChameleonDeviceError(CHAMELEON_DEVICE_EMPTY_CONFIG_MSG)
48    elif not isinstance(configs, list):
49        raise ChameleonDeviceError(CHAMELEON_DEVICE_NOT_LIST_CONFIG_MSG)
50    elif isinstance(configs[0], str):
51        # Configs is a list of IP addresses
52        chameleons = get_instances(configs)
53    return chameleons
54
55
56def destroy(chameleons):
57    for chameleon in chameleons:
58        del chameleon
59
60
61def get_info(chameleons):
62    """Get information on a list of ChameleonDevice objects.
63
64    Args:
65        ads: A list of ChameleonDevice objects.
66
67    Returns:
68        A list of dict, each representing info for ChameleonDevice objects.
69    """
70    device_info = []
71    for chameleon in chameleons:
72        info = {"address": chameleon.address, "port": chameleon.port}
73        device_info.append(info)
74    return device_info
75
76
77def get_instances(ips):
78    """Create ChameleonDevice instances from a list of IPs.
79
80    Args:
81        ips: A list of Chameleon IPs.
82
83    Returns:
84        A list of ChameleonDevice objects.
85    """
86    return [ChameleonDevice(ip) for ip in ips]
87
88
89class ChameleonDevice:
90    """Class representing a Chameleon device.
91
92    Each object of this class represents one Chameleon device in ACTS.
93
94    Attributes:
95        address: The full address to contact the Chameleon device at
96        client: The ServiceProxy of the XMLRPC client.
97        log: A logger object.
98        port: The TCP port number of the Chameleon device.
99    """
100
101    def __init__(self, ip="", port=9992):
102        self.ip = ip
103        self.log = logging.getLogger()
104        self.port = port
105        self.address = "http://{}:{}".format(ip, self.port)
106        try:
107            self.client = xmlrpc.client.ServerProxy(self.address,
108                                                    allow_none=True,
109                                                    verbose=False)
110        except ConnectionRefusedError as err:
111            self.log.exception(
112                "Failed to connect to Chameleon Device at: {}".format(
113                    self.address))
114        self.client.Reset()
115
116    def pull_file(self, chameleon_location, destination):
117        """Pulls a file from the Chameleon device. Usually the raw audio file.
118
119        Args:
120            chameleon_location: The path to the file on the Chameleon device
121            destination: The destination to where to pull it locally.
122        """
123        # TODO: (tturney) implement
124        self.log.error("Definition not yet implemented")
125
126    def start_capturing_audio(self, port_id, has_file=True):
127        """Starts capturing audio.
128
129        Args:
130            port_id: The ID of the audio input port.
131            has_file: True for saving audio data to file. False otherwise.
132        """
133        self.client.StartCapturingAudio(port_id, has_file)
134
135    def stop_capturing_audio(self, port_id):
136        """Stops capturing audio.
137
138        Args:
139            port_id: The ID of the audio input port.
140        Returns:
141            List contain the location of the recorded audio and a dictionary
142            of values relating to the raw audio including: file_type, channel,
143            sample_format, and rate.
144        """
145        return self.client.StopCapturingAudio(port_id)
146
147    def audio_board_connect(self, bus_number, endpoint):
148        """Connects an endpoint to an audio bus.
149
150        Args:
151            bus_number: 1 or 2 for audio bus 1 or bus 2.
152            endpoint: An endpoint defined in audio_bus_endpoints.
153        """
154        self.client.AudioBoardConnect(bus_number, endpoint)
155
156    def audio_board_disconnect(self, bus_number, endpoint):
157        """Connects an endpoint to an audio bus.
158
159        Args:
160            bus_number: 1 or 2 for audio bus 1 or bus 2.
161            endpoint: An endpoint defined in audio_bus_endpoints.
162        """
163        self.client.AudioBoardDisconnect(bus_number, endpoint)
164
165    def audio_board_disable_bluetooth(self):
166        """Disables Bluetooth module on audio board."""
167        self.client.AudioBoardDisableBluetooth()
168
169    def audio_board_clear_routes(self, bus_number):
170        """Clears routes on an audio bus.
171
172        Args:
173            bus_number: 1 or 2 for audio bus 1 or bus 2.
174        """
175        self.client.AudioBoardClearRoutes(bus_number)
176
177    def scp(self, source, destination):
178        """Copies files from the Chameleon device to the host machine.
179
180        Args:
181            source: The file path on the Chameleon board.
182            dest: The file path on the host machine.
183        """
184        cmd = "scp root@{}:/{} {}".format(self.ip, source, destination)
185        try:
186            call(cmd.split(" "))
187        except FileNotFoundError as err:
188            self.log.exception("File not found {}".format(source))
189