1# Copyright 2024, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Module that detects device attributes and USB speed using adb commands.""" 16 17import enum 18import logging 19import subprocess 20from typing import NamedTuple 21from atest import atest_utils 22from atest import constants 23 24 25@enum.unique 26class UsbAttributeName(enum.Enum): 27 NEGOTIATED_SPEED = 'current_speed' 28 MAXIMUM_SPEED = 'maximum_speed' 29 30 31class DeviceIds(NamedTuple): 32 manufacturer: str 33 model: str 34 name: str 35 serial: str 36 address: str 37 38 39def verify_and_print_usb_speed_warning( 40 device_ids: DeviceIds, negotiated_speed: int, max_speed: int 41) -> bool: 42 """Checks whether the connection speed is optimal for the given device. 43 44 Args: 45 device_ids: Identifiers allowing a user to recognize the device the usb 46 speed warning is related to. 47 negotiated_speed: The current speed of the device. 48 max_speed: The maximum speed that the given device is capable of. 49 50 Returns: 51 True if the warning was printed, False otherwise. 52 """ 53 # If a USB-2 is used with a USB-3 capable device, the speed will be 54 # downgraded to 480 Mbps and never 12 Mbps, so this is the only case we 55 # check. 56 if negotiated_speed == 480 and negotiated_speed < max_speed: 57 _print_usb_speed_warning(device_ids, negotiated_speed, max_speed) 58 return True 59 return False 60 61 62def _print_usb_speed_warning( 63 device_ids: DeviceIds, negotiated_speed: int, max_speed: int 64): 65 """Prints a warning about the device's operating speed if it's suboptimal. 66 67 Args: 68 device_ids: Identifiers allowing a user to recognize the device the usb 69 speed warning is related to. 70 negotiated_speed: The negotiated speed (in Mbits per seconds) the device is 71 operating at. 72 max_speed: The maximum speed (in Mbits per seconds) of which the device is 73 capable. 74 """ 75 atest_utils.colorful_print( 76 f'Warning: The {device_ids.manufacturer} {device_ids.model} device (' 77 f'{device_ids.name}) with address {device_ids.address} and serial ' 78 f'{device_ids.serial} is using ' 79 f'{_speed_to_string(negotiated_speed)} while ' 80 f'{_speed_to_string(max_speed)} capable. Check the USB cables/hubs.', 81 constants.MAGENTA, 82 ) 83 84 85def _speed_to_string(speed: int) -> str: 86 """Converts a speed in Mbps to a string.""" 87 return { 88 480: 'USB-2 (480 Mbps)', 89 5000: 'USB-3.0 (5,000 Mbps)', 90 10000: 'USB-3.1 (10,000 Mbps)', 91 20000: 'USB-3.2 (20,000 Mbps)', 92 40000: 'USB-4.0 (40,000 Mbps)', 93 }.get(speed, f'{speed:,} Mbps') 94 95 96def _string_to_speed(speed_str: str) -> int: 97 return { 98 'UNKNOWN': 0, 99 'high-speed': 480, 100 'super-speed': 5000, 101 'super-speed-plus': 10000, 102 }.get(speed_str, 0) 103 104 105def get_udc_driver_usb_device_dir_name() -> str: 106 """Reads the directory where the usb devices attributes are stored. 107 108 Returns: 109 A string corresponding to the directory name. 110 """ 111 return _adb_read_file('/config/usb_gadget/g1/UDC') 112 113 114def get_udc_driver_usb_device_attribute_speed_value( 115 speed_dir_name: str, 116 attr_name: UsbAttributeName, 117) -> int: 118 """Reads the usb speed string from the device and returns the numeric speed. 119 120 Args: 121 speed_dir_name: name of the directory where the usb driver attributes are 122 located. 123 attr_name: The attribute to read from the device. 124 125 Returns: 126 An int corresponding to the numeric speed value converted from the udc 127 driver attribute value. 0 is returned if adb is unable to read the value. 128 """ 129 speed_reading = _adb_read_file( 130 '/sys/class/udc/' + speed_dir_name + '/' + attr_name.value 131 ) 132 return _string_to_speed(speed_reading) 133 134 135def _adb_read_file(file_path: str) -> str: 136 cmd = [ 137 'adb', 138 'shell', 139 'su', 140 '0', 141 f'cat {file_path}', 142 ] 143 try: 144 logging.debug('Running command: %s', cmd) 145 result = subprocess.check_output( 146 cmd, 147 encoding='utf-8', 148 stderr=subprocess.STDOUT, 149 ) 150 return result.strip() 151 except subprocess.CalledProcessError as cpe: 152 logging.debug( 153 f'Cannot read directory; USB speed will not be read. Error: %s', cpe 154 ) 155 except OSError as ose: 156 logging.debug(f'Cannot read usb speed from the device. Error: %s', ose) 157 return '' 158 159 160def get_adb_device_identifiers() -> DeviceIds | None: 161 """Fetch the user-facing device identifiers.""" 162 if not atest_utils.has_command('adb'): 163 return None 164 165 device_serial = _adb_run_cmd(['adb', 'shell', 'getprop', 'ro.serialno']) 166 if not device_serial: 167 return None 168 169 device_address_resp = _adb_run_cmd(['adb', 'devices']) 170 try: 171 device_addresses = device_address_resp.splitlines() 172 for line in device_addresses: 173 if 'device' in line: 174 device_address = line.split()[0].strip() 175 except IndexError: 176 logging.debug('No devices are connected. USB speed will not be read.') 177 return None 178 179 device_manufacturer = _adb_run_cmd( 180 ['adb', 'shell', 'getprop', 'ro.product.manufacturer'] 181 ) 182 device_model = _adb_run_cmd(['adb', 'shell', 'getprop', 'ro.product.model']) 183 device_name = _adb_run_cmd(['adb', 'shell', 'getprop', 'ro.product.name']) 184 185 return DeviceIds( 186 manufacturer=device_manufacturer, 187 model=device_model, 188 name=device_name, 189 serial=device_serial, 190 address=device_address, 191 ) 192 193 194def _adb_run_cmd(cmd: list[str]) -> str: 195 try: 196 logging.debug(f'Running command: %s.', cmd) 197 result = subprocess.check_output( 198 cmd, 199 encoding='utf-8', 200 stderr=subprocess.STDOUT, 201 ) 202 return result.strip() if result else '' 203 except subprocess.CalledProcessError: 204 logging.debug( 205 'Exception raised while running `%s`. USB speed will not be read.', cmd 206 ) 207 except OSError: 208 logging.debug('Could not find adb. USB speed will not be read.') 209 return '' 210