1#!/usr/bin/env python3
2#
3#   Copyright 2018 - 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 os
19import re
20import serial
21import subprocess
22import threading
23import time
24
25from acts import logger
26from acts import signals
27from acts import tracelogger
28from acts import utils
29from acts.test_utils.wifi import wifi_test_utils as wutils
30
31from datetime import datetime
32
33ACTS_CONTROLLER_CONFIG_NAME = "ArduinoWifiDongle"
34ACTS_CONTROLLER_REFERENCE_NAME = "arduino_wifi_dongles"
35
36WIFI_DONGLE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
37WIFI_DONGLE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
38
39DEV = "/dev/"
40IP = "IP: "
41STATUS = "STATUS: "
42SSID = "SSID: "
43RSSI = "RSSI: "
44PING = "PING: "
45SCAN_BEGIN = "Scan Begin"
46SCAN_END = "Scan End"
47READ_TIMEOUT = 10
48BAUD_RATE = 9600
49TMP_DIR = "tmp/"
50SSID_KEY = wutils.WifiEnums.SSID_KEY
51PWD_KEY = wutils.WifiEnums.PWD_KEY
52
53
54class ArduinoWifiDongleError(signals.ControllerError):
55    pass
56
57class DoesNotExistError(ArduinoWifiDongleError):
58    """Raised when something that does not exist is referenced."""
59
60def create(configs):
61    """Creates ArduinoWifiDongle objects.
62
63    Args:
64        configs: A list of dicts or a list of serial numbers, each representing
65                 a configuration of a arduino wifi dongle.
66
67    Returns:
68        A list of Wifi dongle objects.
69    """
70    wcs = []
71    if not configs:
72        raise ArduinoWifiDongleError(WIFI_DONGLE_EMPTY_CONFIG_MSG)
73    elif not isinstance(configs, list):
74        raise ArduinoWifiDongleError(WIFI_DONGLE_NOT_LIST_CONFIG_MSG)
75    elif isinstance(configs[0], str):
76        # Configs is a list of serials.
77        wcs = get_instances(configs)
78    else:
79        # Configs is a list of dicts.
80        wcs = get_instances_with_configs(configs)
81
82    return wcs
83
84def destroy(wcs):
85    for wc in wcs:
86        wc.clean_up()
87
88def get_instances(configs):
89    wcs = []
90    for s in configs:
91        wcs.append(ArduinoWifiDongle(s))
92    return wcs
93
94def get_instances_with_configs(configs):
95    wcs = []
96    for c in configs:
97        try:
98            s = c.pop("serial")
99        except KeyError:
100            raise ArduinoWifiDongleError(
101                "'serial' is missing for ArduinoWifiDongle config %s." % c)
102        wcs.append(ArduinoWifiDongle(s))
103    return wcs
104
105class ArduinoWifiDongle(object):
106    """Class representing an arduino wifi dongle.
107
108    Each object of this class represents one wifi dongle in ACTS.
109
110    Attribtues:
111        serial: Short serial number of the wifi dongle in string.
112        port: The terminal port the dongle is connected to in string.
113        log: A logger adapted from root logger with added token specific to an
114             ArduinoWifiDongle instance.
115        log_file_fd: File handle of the log file.
116        set_logging: Logging for the dongle is enabled when this param is set
117        lock: Lock to acquire and release set_logging variable
118        ssid: SSID of the wifi network the dongle is connected to.
119        ip_addr: IP address on the wifi interface.
120        scan_results: Most recent scan results.
121        ping: Ping status in bool - ping to www.google.com
122    """
123    def __init__(self, serial=''):
124        """Initializes the ArduinoWifiDongle object."""
125        self.serial = serial
126        self.port = self._get_serial_port()
127        self.log = logger.create_tagged_trace_logger(
128            "ArduinoWifiDongle|%s" % self.serial)
129        log_path_base = getattr(logging, "log_path", "/tmp/logs")
130        self.log_file_path = os.path.join(
131            log_path_base, "ArduinoWifiDongle_%s_serial_log.txt" % self.serial)
132        self.log_file_fd = open(self.log_file_path, "a")
133
134        self.set_logging = True
135        self.lock = threading.Lock()
136        self.start_controller_log()
137
138        self.ssid = None
139        self.ip_addr = None
140        self.status = 0
141        self.scan_results = []
142        self.scanning = False
143        self.ping = False
144
145        try:
146            os.stat(TMP_DIR)
147        except:
148            os.mkdir(TMP_DIR)
149
150    def clean_up(self):
151        """Cleans up the ArduinoifiDongle object and releases any resources it
152        claimed.
153        """
154        self.stop_controller_log()
155        self.log_file_fd.close()
156
157    def _get_serial_port(self):
158        """Get the serial port for a given ArduinoWifiDongle serial number.
159
160        Returns:
161            Serial port in string if the dongle is attached.
162        """
163        if not self.serial:
164            raise ArduinoWifiDongleError(
165                "Wifi dongle's serial should not be empty")
166        cmd = "ls %s" % DEV
167        serial_ports = utils.exe_cmd(cmd).decode("utf-8", "ignore").split("\n")
168        for port in serial_ports:
169            if "USB" not in port:
170                continue
171            tty_port = "%s%s" % (DEV, port)
172            cmd = "udevadm info %s" % tty_port
173            udev_output = utils.exe_cmd(cmd).decode("utf-8", "ignore")
174            result = re.search("ID_SERIAL_SHORT=(.*)\n", udev_output)
175            if self.serial == result.group(1):
176                logging.info("Found wifi dongle %s at serial port %s" %
177                             (self.serial, tty_port))
178                return tty_port
179        raise ArduinoWifiDongleError("Wifi dongle %s is specified in config"
180                                    " but is not attached." % self.serial)
181
182    def write(self, arduino, file_path, network=None):
183        """Write an ino file to the arduino wifi dongle.
184
185        Args:
186            arduino: path of the arduino executable.
187            file_path: path of the ino file to flash onto the dongle.
188            network: wifi network to connect to.
189
190        Returns:
191            True: if the write is sucessful.
192            False: if not.
193        """
194        return_result = True
195        self.stop_controller_log("Flashing %s\n" % file_path)
196        cmd = arduino + file_path + " --upload --port " + self.port
197        if network:
198            cmd = self._update_ino_wifi_network(arduino, file_path, network)
199        self.log.info("Command is %s" % cmd)
200        proc = subprocess.Popen(cmd,
201            stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
202        out, err = proc.communicate()
203        return_code = proc.returncode
204        if return_code != 0:
205            self.log.error("Failed to write file %s" % return_code)
206            return_result = False
207        self.start_controller_log("Flashing complete\n")
208        return return_result
209
210    def _update_ino_wifi_network(self, arduino, file_path, network):
211        """Update wifi network in the ino file.
212
213        Args:
214            arduino: path of the arduino executable.
215            file_path: path of the ino file to flash onto the dongle
216            network: wifi network to update the ino file with
217
218        Returns:
219            cmd: arduino command to run to flash the ino file
220        """
221        tmp_file = "%s%s" % (TMP_DIR, file_path.split('/')[-1])
222        utils.exe_cmd("cp %s %s" % (file_path, tmp_file))
223        ssid = network[SSID_KEY]
224        pwd = network[PWD_KEY]
225        sed_cmd = "sed -i 's/\"wifi_tethering_test\"/\"%s\"/' %s" % (ssid, tmp_file)
226        utils.exe_cmd(sed_cmd)
227        sed_cmd = "sed -i  's/\"password\"/\"%s\"/' %s" % (pwd, tmp_file)
228        utils.exe_cmd(sed_cmd)
229        cmd = "%s %s --upload --port %s" %(arduino, tmp_file, self.port)
230        return cmd
231
232    def start_controller_log(self, msg=None):
233        """Reads the serial port and writes the data to ACTS log file.
234
235        This method depends on the logging enabled in the .ino files. The logs
236        are read from the serial port and are written to the ACTS log after
237        adding a timestamp to the data.
238
239        Args:
240            msg: Optional param to write to the log file.
241        """
242        if msg:
243            curr_time = str(datetime.now())
244            self.log_file_fd.write(curr_time + " INFO: " + msg)
245        t = threading.Thread(target=self._start_log)
246        t.daemon = True
247        t.start()
248
249    def stop_controller_log(self, msg=None):
250        """Stop the controller log.
251
252        Args:
253            msg: Optional param to write to the log file.
254        """
255        with self.lock:
256            self.set_logging = False
257        if msg:
258            curr_time = str(datetime.now())
259            self.log_file_fd.write(curr_time + " INFO: " + msg)
260
261    def _start_log(self):
262        """Target method called by start_controller_log().
263
264        This method is called as a daemon thread, which continously reads the
265        serial port. Stops when set_logging is set to False or when the test
266        ends.
267        """
268        self.set_logging = True
269        ser = serial.Serial(self.port, BAUD_RATE)
270        while True:
271            curr_time = str(datetime.now())
272            data = ser.readline().decode("utf-8", "ignore")
273            self._set_vars(data)
274            with self.lock:
275                if not self.set_logging:
276                    break
277            self.log_file_fd.write(curr_time + " " + data)
278
279    def _set_vars(self, data):
280        """Sets the variables by reading from the serial port.
281
282        Wifi dongle data such as wifi status, ip address, scan results
283        are read from the serial port and saved inside the class.
284
285        Args:
286            data: New line from the serial port.
287        """
288        # 'data' represents each line retrieved from the device's serial port.
289        # since we depend on the serial port logs to get the attributes of the
290        # dongle, every line has the format of {ino_file: method: param: value}.
291        # We look for the attribute in the log and retrieve its value.
292        # Ex: data = "connect_wifi: loop(): STATUS: 3" then val = "3"
293        # Similarly, we check when the scan has begun and ended and get all the
294        # scan results in between.
295        if data.count(":") != 3:
296            return
297        val = data.split(":")[-1].lstrip().rstrip()
298        if SCAN_BEGIN in data:
299            self.scan_results = []
300            self.scanning = True
301        elif SCAN_END in data:
302            self.scanning = False
303        elif self.scanning:
304            self.scan_results.append(data)
305        elif IP in data:
306            self.ip_addr = None if val == "0.0.0.0" else val
307        elif SSID in data:
308            self.ssid = val
309        elif STATUS in data:
310            self.status = int(val)
311        elif PING in data:
312            self.ping = False if int(val) == 0 else True
313
314    def ip_address(self, exp_result=True, timeout=READ_TIMEOUT):
315        """Get the ip address of the wifi dongle.
316
317        Args:
318            exp_result: True if IP address is expected (wifi connected).
319            timeout: Optional param that specifies the wait time for the IP
320                     address to come up on the dongle.
321
322        Returns:
323            IP: addr in string, if wifi connected.
324                None if not connected.
325        """
326        curr_time = time.time()
327        while time.time() < curr_time + timeout:
328            if (exp_result and self.ip_addr) or \
329                (not exp_result and not self.ip_addr):
330                  break
331            time.sleep(1)
332        return self.ip_addr
333
334    def wifi_status(self, exp_result=True, timeout=READ_TIMEOUT):
335        """Get wifi status on the dongle.
336
337        Returns:
338            True: if wifi is connected.
339            False: if not connected.
340        """
341        curr_time = time.time()
342        while time.time() < curr_time + timeout:
343            if (exp_result and self.status == 3) or \
344                (not exp_result and not self.status):
345                  break
346            time.sleep(1)
347        return self.status == 3
348
349    def wifi_scan(self, exp_result=True, timeout=READ_TIMEOUT):
350        """Get the wifi scan results.
351
352        Args:
353            exp_result: True if scan results are expected.
354            timeout: Optional param that specifies the wait time for the scan
355                     results to come up on the dongle.
356
357        Returns:
358            list of dictionaries each with SSID and RSSI of the network
359            found in the scan.
360        """
361        scan_networks = []
362        d = {}
363        curr_time = time.time()
364        while time.time() < curr_time + timeout:
365            if (exp_result and self.scan_results) or \
366                (not exp_result and not self.scan_results):
367                  break
368            time.sleep(1)
369        for i in range(len(self.scan_results)):
370            if SSID in self.scan_results[i]:
371                d = {}
372                d[SSID] = self.scan_results[i].split(":")[-1].rstrip()
373            elif RSSI in self.scan_results[i]:
374                d[RSSI] = self.scan_results[i].split(":")[-1].rstrip()
375                scan_networks.append(d)
376
377        return scan_networks
378
379    def ping_status(self, exp_result=True, timeout=READ_TIMEOUT):
380        """ Get ping status on the dongle.
381
382        Returns:
383            True: if ping is successful
384            False: if not successful
385        """
386        curr_time = time.time()
387        while time.time() < curr_time + timeout:
388            if (exp_result and self.ping) or \
389                (not exp_result and not self.ping):
390                  break
391            time.sleep(1)
392        return self.ping
393