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