Home | History | Annotate | Download | only in controllers
      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 
     17 import logging
     18 import os
     19 import re
     20 import serial
     21 import subprocess
     22 import threading
     23 import time
     24 
     25 from acts import logger
     26 from acts import signals
     27 from acts import tracelogger
     28 from acts import utils
     29 from acts.test_utils.wifi import wifi_test_utils as wutils
     30 
     31 from datetime import datetime
     32 
     33 ACTS_CONTROLLER_CONFIG_NAME = "ArduinoWifiDongle"
     34 ACTS_CONTROLLER_REFERENCE_NAME = "arduino_wifi_dongles"
     35 
     36 WIFI_DONGLE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
     37 WIFI_DONGLE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
     38 
     39 DEV = "/dev/"
     40 IP = "IP: "
     41 STATUS = "STATUS: "
     42 SSID = "SSID: "
     43 RSSI = "RSSI: "
     44 PING = "PING: "
     45 SCAN_BEGIN = "Scan Begin"
     46 SCAN_END = "Scan End"
     47 READ_TIMEOUT = 10
     48 BAUD_RATE = 9600
     49 TMP_DIR = "tmp/"
     50 SSID_KEY = wutils.WifiEnums.SSID_KEY
     51 PWD_KEY = wutils.WifiEnums.PWD_KEY
     52 
     53 
     54 class ArduinoWifiDongleError(signals.ControllerError):
     55     pass
     56 
     57 class DoesNotExistError(ArduinoWifiDongleError):
     58     """Raised when something that does not exist is referenced."""
     59 
     60 def 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 
     84 def destroy(wcs):
     85     for wc in wcs:
     86         wc.clean_up()
     87 
     88 def get_instances(configs):
     89     wcs = []
     90     for s in configs:
     91         wcs.append(ArduinoWifiDongle(s))
     92     return wcs
     93 
     94 def 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 
    105 class 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