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