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