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