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 subprocess
21import threading
22import time
23from datetime import datetime
24
25from serial import Serial
26
27from acts import logger
28from acts import signals
29from acts import utils
30from acts.test_utils.wifi import wifi_test_utils as wutils
31
32ACTS_CONTROLLER_CONFIG_NAME = 'ArduinoWifiDongle'
33ACTS_CONTROLLER_REFERENCE_NAME = 'arduino_wifi_dongles'
34
35WIFI_DONGLE_EMPTY_CONFIG_MSG = 'Configuration is empty, abort!'
36WIFI_DONGLE_NOT_LIST_CONFIG_MSG = 'Configuration should be a list, abort!'
37
38DEV = '/dev/'
39IP = 'IP: '
40STATUS = 'STATUS: '
41SSID = 'SSID: '
42RSSI = 'RSSI: '
43PING = 'PING: '
44SCAN_BEGIN = 'Scan Begin'
45SCAN_END = 'Scan End'
46READ_TIMEOUT = 10
47BAUD_RATE = 9600
48TMP_DIR = 'tmp/'
49SSID_KEY = wutils.WifiEnums.SSID_KEY
50PWD_KEY = wutils.WifiEnums.PWD_KEY
51
52
53class ArduinoWifiDongleError(signals.ControllerError):
54    pass
55
56
57def create(configs):
58    """Creates ArduinoWifiDongle objects.
59
60    Args:
61        configs: A list of dicts or a list of serial numbers, each representing
62                 a configuration of a arduino wifi dongle.
63
64    Returns:
65        A list of Wifi dongle objects.
66    """
67    if not configs:
68        raise ArduinoWifiDongleError(WIFI_DONGLE_EMPTY_CONFIG_MSG)
69    elif not isinstance(configs, list):
70        raise ArduinoWifiDongleError(WIFI_DONGLE_NOT_LIST_CONFIG_MSG)
71    elif isinstance(configs[0], str):
72        # Configs is a list of serials.
73        return get_instances(configs)
74    else:
75        # Configs is a list of dicts.
76        return get_instances_with_configs(configs)
77
78
79def destroy(wcs):
80    for wc in wcs:
81        wc.clean_up()
82
83
84def get_instances(configs):
85    wcs = []
86    for s in configs:
87        wcs.append(ArduinoWifiDongle(s))
88    return wcs
89
90
91def get_instances_with_configs(configs):
92    wcs = []
93    for c in configs:
94        try:
95            s = c.pop('serial')
96        except KeyError:
97            raise ArduinoWifiDongleError(
98                '"serial" is missing for ArduinoWifiDongle config %s.' % c)
99        wcs.append(ArduinoWifiDongle(s))
100    return wcs
101
102
103class ArduinoWifiDongle(object):
104    """Class representing an arduino wifi dongle.
105
106    Each object of this class represents one wifi dongle in ACTS.
107
108    Attribtues:
109        serial: Short serial number of the wifi dongle in string.
110        port: The terminal port the dongle is connected to in string.
111        log: A logger adapted from root logger with added token specific to an
112             ArduinoWifiDongle instance.
113        log_file_fd: File handle of the log file.
114        set_logging: Logging for the dongle is enabled when this param is set
115        lock: Lock to acquire and release set_logging variable
116        ssid: SSID of the wifi network the dongle is connected to.
117        ip_addr: IP address on the wifi interface.
118        scan_results: Most recent scan results.
119        ping: Ping status in bool - ping to www.google.com
120    """
121
122    def __init__(self, serial):
123        """Initializes the ArduinoWifiDongle object.
124
125        Args:
126            serial: The serial number for the wifi dongle.
127        """
128        if not serial:
129            raise ArduinoWifiDongleError(
130                'The ArduinoWifiDongle serial number must not be empty.')
131        self.serial = serial
132        self.port = self._get_serial_port()
133        self.log = logger.create_tagged_trace_logger(
134            'ArduinoWifiDongle|%s' % self.serial)
135        log_path_base = getattr(logging, 'log_path', '/tmp/logs')
136        self.log_file_path = os.path.join(
137            log_path_base, 'ArduinoWifiDongle_%s_serial_log.txt' % self.serial)
138        self.log_file_fd = open(self.log_file_path, 'a')
139
140        self.set_logging = True
141        self.lock = threading.Lock()
142        self.start_controller_log()
143
144        self.ssid = None
145        self.ip_addr = None
146        self.status = 0
147        self.scan_results = []
148        self.scanning = False
149        self.ping = False
150
151        os.makedirs(TMP_DIR, exist_ok=True)
152
153    def clean_up(self):
154        """Cleans up the controller and releases any resources it claimed."""
155        self.stop_controller_log()
156        self.log_file_fd.close()
157
158    def _get_serial_port(self):
159        """Get the serial port for a given ArduinoWifiDongle serial number.
160
161        Returns:
162            Serial port in string if the dongle is attached.
163        """
164        cmd = 'ls %s' % DEV
165        serial_ports = utils.exe_cmd(cmd).decode('utf-8', 'ignore').split('\n')
166        for port in serial_ports:
167            if 'USB' not in port:
168                continue
169            tty_port = '%s%s' % (DEV, port)
170            cmd = 'udevadm info %s' % tty_port
171            udev_output = utils.exe_cmd(cmd).decode('utf-8', 'ignore')
172            result = re.search('ID_SERIAL_SHORT=(.*)\n', udev_output)
173            if self.serial == result.group(1):
174                logging.info('Found wifi dongle %s at serial port %s' %
175                             (self.serial, tty_port))
176                return tty_port
177        raise ArduinoWifiDongleError('Wifi dongle %s is specified in config'
178                                     ' but is not attached.' % self.serial)
179
180    def write(self, arduino, file_path, network=None):
181        """Write an ino file to the arduino wifi dongle.
182
183        Args:
184            arduino: path of the arduino executable.
185            file_path: path of the ino file to flash onto the dongle.
186            network: wifi network to connect to.
187
188        Returns:
189            True: if the write is sucessful.
190            False: if not.
191        """
192        return_result = True
193        self.stop_controller_log('Flashing %s\n' % file_path)
194        cmd = arduino + file_path + ' --upload --port ' + self.port
195        if network:
196            cmd = self._update_ino_wifi_network(arduino, file_path, network)
197        self.log.info('Command is %s' % cmd)
198        proc = subprocess.Popen(cmd,
199                                stdout=subprocess.PIPE, stderr=subprocess.PIPE,
200                                shell=True)
201        _, _ = proc.communicate()
202        return_code = proc.returncode
203        if return_code != 0:
204            self.log.error('Failed to write file %s' % return_code)
205            return_result = False
206        self.start_controller_log('Flashing complete\n')
207        return return_result
208
209    def _update_ino_wifi_network(self, arduino, file_path, network):
210        """Update wifi network in the ino file.
211
212        Args:
213            arduino: path of the arduino executable.
214            file_path: path of the ino file to flash onto the dongle
215            network: wifi network to update the ino file with
216
217        Returns:
218            cmd: arduino command to run to flash the .ino file
219        """
220        tmp_file = '%s%s' % (TMP_DIR, file_path.split('/')[-1])
221        utils.exe_cmd('cp %s %s' % (file_path, tmp_file))
222        ssid = network[SSID_KEY]
223        pwd = network[PWD_KEY]
224        sed_cmd = 'sed -i \'s/"wifi_tethering_test"/"%s"/\' %s' % (
225            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 continuously 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(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 = int(val) != 0
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.clear()
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 (not exp_result and not self.ping):
389                break
390            time.sleep(1)
391        return self.ping
392