xref: /aosp_15_r20/tools/asuite/atest/usb_speed_detect.py (revision c2e18aaa1096c836b086f94603d04f4eb9cf37f5)
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