1#  Copyright (C) 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"""Constants for Nearby Connection."""
16
17import ast
18import dataclasses
19import datetime
20import enum
21import logging
22from typing import Any
23
24SUCCESS_RATE_TARGET = 0.98
25MCC_PERFORMANCE_TEST_COUNT = 100
26MCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR = 5
27SCC_PERFORMANCE_TEST_COUNT = 10
28SCC_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR = 2
29BT_PERFORMANCE_TEST_COUNT = 100
30BT_PERFORMANCE_TEST_MAX_CONSECUTIVE_ERROR = 5
31
32TARGET_POST_WIFI_CONNECTION_IDLE_TIME_SEC = 10
33
34NEARBY_RESET_WAIT_TIME = datetime.timedelta(seconds=2)
35WIFI_DISCONNECTION_DELAY = datetime.timedelta(seconds=1)
36
37FIRST_DISCOVERY_TIMEOUT = datetime.timedelta(seconds=30)
38FIRST_CONNECTION_INIT_TIMEOUT = datetime.timedelta(seconds=30)
39FIRST_CONNECTION_RESULT_TIMEOUT = datetime.timedelta(seconds=35)
40BT_1K_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=20)
41BT_500K_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=25)
42BLE_500K_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=25)
43SECOND_DISCOVERY_TIMEOUT = datetime.timedelta(seconds=35)
44SECOND_CONNECTION_INIT_TIMEOUT = datetime.timedelta(seconds=10)
45SECOND_CONNECTION_RESULT_TIMEOUT = datetime.timedelta(seconds=25)
46CONNECTION_BANDWIDTH_CHANGED_TIMEOUT = datetime.timedelta(seconds=25)
47WIFI_1K_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=20)
48WIFI_2G_20M_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=20)
49WIFI_200M_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=100)
50WIFI_500M_PAYLOAD_TRANSFER_TIMEOUT = datetime.timedelta(seconds=250)
51WIFI_STA_CONNECTING_TIME_OUT = datetime.timedelta(seconds=25)
52DISCONNECTION_TIMEOUT = datetime.timedelta(seconds=15)
53
54MAX_PHY_RATE_PER_STREAM_AC_80_MBPS = 433
55MAX_PHY_RATE_PER_STREAM_AC_40_MBPS = 200
56MAX_PHY_RATE_PER_STREAM_N_20_MBPS = 72
57
58MCC_THROUGHPUT_MULTIPLIER = 0.25
59MAX_PHY_RATE_TO_MIN_THROUGHPUT_RATIO_5G = 0.37
60MAX_PHY_RATE_TO_MIN_THROUGHPUT_RATIO_2G = 0.10
61WLAN_THROUGHPUT_CAP_MBPS = 20  # cap for WLAN medium due to encryption overhead
62
63CLASSIC_BT_MEDIUM_THROUGHPUT_BENCHMARK_MBPS = 0.02
64BLE_MEDIUM_THROUGHPUT_BENCHMARK_MBPS = 0.02
65
66KEEP_ALIVE_TIMEOUT_BT_MS = 30000
67KEEP_ALIVE_INTERVAL_BT_MS = 5000
68
69KEEP_ALIVE_TIMEOUT_WIFI_MS = 10000
70KEEP_ALIVE_INTERVAL_WIFI_MS = 3000
71
72PERCENTILE_50_FACTOR = 0.5
73LATENCY_PRECISION_DIGITS = 1
74
75UNSET_LATENCY = datetime.timedelta.max
76UNSET_THROUGHPUT_KBPS = -1.0
77MAX_NUM_BUG_REPORT = 5
78INVALID_INT = -1
79
80TRANSFER_FILE_SIZE_500MB = 500 * 1024  # kB
81TRANSFER_FILE_SIZE_200MB = 200 * 1024  # kB
82TRANSFER_FILE_SIZE_20MB = 20 * 1024  # kB
83TRANSFER_FILE_SIZE_1MB = 1024  # kB
84TRANSFER_FILE_SIZE_500KB = 512  # kB
85TRANSFER_FILE_SIZE_1KB = 1  # kB
86
87TARGET_CUJ_QUICK_START = 'quick_start'
88TARGET_CUJ_ESIM = 'setting_based_esim_transfer'
89TARGET_CUJ_QUICK_SHARE = 'quick_share'
90
91
92@enum.unique
93class PayloadType(enum.IntEnum):
94  FILE = 2
95  STREAM = 3
96
97
98@enum.unique
99class NearbyMedium(enum.IntEnum):
100  """Medium options for discovery, advertising, connection and upgrade."""
101
102  AUTO = 0
103  BT_ONLY = 1
104  BLE_ONLY = 2
105  WIFILAN_ONLY = 3
106  WIFIAWARE_ONLY = 4
107  UPGRADE_TO_WEBRTC = 5
108  UPGRADE_TO_WIFIHOTSPOT = 6
109  UPGRADE_TO_WIFIDIRECT = 7
110  BLE_L2CAP_ONLY = 8
111  # including WIFI_LAN, WIFI_HOTSPOT, WIFI_DIRECT
112  UPGRADE_TO_ALL_WIFI = 9
113
114
115@dataclasses.dataclass(frozen=False)
116class TestParameters:
117  """Test parameters to be customized for Nearby Connection."""
118
119  target_cuj_name: str = 'unspecified'
120  requires_bt_multiplex: bool = False
121  requires_3p_api_test: bool = False
122  abort_all_tests_on_function_tests_fail: bool = True
123  fast_fail_on_any_error: bool = False
124  use_auto_controlled_wifi_ap: bool = False
125  wifi_2g_ssid: str = ''
126  wifi_2g_password: str = ''
127  wifi_5g_ssid: str = ''
128  wifi_5g_password: str = ''
129  wifi_dfs_5g_ssid: str = ''
130  wifi_dfs_5g_password: str = ''
131  wifi_ssid: str = ''  # optional, for tests which can use any wifi
132  wifi_password: str = ''
133  advertising_discovery_medium: NearbyMedium = NearbyMedium.BLE_ONLY
134  connection_medium: NearbyMedium = NearbyMedium.BT_ONLY
135  toggle_airplane_mode_target_side: bool = False
136  reset_wifi_connection: bool = True
137  disconnect_bt_after_test: bool = False
138  disconnect_wifi_after_test: bool = False
139  payload_type: PayloadType = PayloadType.FILE
140  allow_unrooted_device: bool = False
141  keep_alive_timeout_ms: int = KEEP_ALIVE_TIMEOUT_WIFI_MS
142  keep_alive_interval_ms: int = KEEP_ALIVE_INTERVAL_WIFI_MS
143  enable_2g_ble_scan_throttling: bool = True
144  target_post_wifi_connection_idle_time_sec: int = (
145      TARGET_POST_WIFI_CONNECTION_IDLE_TIME_SEC
146  )
147
148  run_function_tests_with_performance_tests: bool = True
149  run_bt_performance_test: bool = True
150  run_ble_performance_test: bool = False
151  run_bt_coex_test: bool = True
152  run_directed_test: bool = True
153  run_compound_test: bool = True
154  run_aware_test: bool = False
155  run_iperf_test: bool = True
156  run_nearby_connections_function_tests: bool = False
157  skip_test_if_wifi_chipset_is_empty: bool = True
158  skip_bug_report: bool = False
159
160  @classmethod
161  def from_user_params(
162      cls,
163      user_params: dict[str, Any]) -> 'TestParameters':
164    """convert the parameters from the testbed to the test parameter."""
165
166    # Convert G3 user int parameter in str format to int.
167    for key, value in user_params.items():
168      if key == 'mh_files':
169        continue
170      logging.info('[Test Parameters] %s: %s', key, value)
171      if value in ('true', 'True'):
172        user_params[key] = True
173      elif value in ('false', 'False'):
174        user_params[key] = False
175      elif isinstance(value, bool):
176        user_params[key] = value
177      elif isinstance(value, str) and value.isdigit():
178        user_params[key] = ast.literal_eval(value)
179
180    test_parameters_names = {
181        field.name for field in dataclasses.fields(cls)
182    }
183    test_parameters = cls(
184        **{
185            key: val
186            for key, val in user_params.items()
187            if key in test_parameters_names
188        }
189    )
190
191    if test_parameters.target_cuj_name == TARGET_CUJ_QUICK_START:
192      test_parameters.requires_bt_multiplex = True
193
194    return test_parameters
195
196
197@enum.unique
198class NearbyConnectionMedium(enum.IntEnum):
199  """The final connection medium selected, see BandWidthInfo.Medium."""
200  UNKNOWN = 0
201  # reserved 1, it's Medium.MDNS, not used now
202  BLUETOOTH = 2
203  WIFI_HOTSPOT = 3
204  BLE = 4
205  WIFI_LAN = 5
206  WIFI_AWARE = 6
207  NFC = 7
208  WIFI_DIRECT = 8
209  WEB_RTC = 9
210  # 10 is reserved.
211  USB = 11
212
213
214def is_high_quality_medium(medium: NearbyMedium) -> bool:
215  return medium in {
216      NearbyMedium.WIFILAN_ONLY,
217      NearbyMedium.WIFIAWARE_ONLY,
218      NearbyMedium.UPGRADE_TO_WEBRTC,
219      NearbyMedium.UPGRADE_TO_WIFIHOTSPOT,
220      NearbyMedium.UPGRADE_TO_WIFIDIRECT,
221      NearbyMedium.UPGRADE_TO_ALL_WIFI,
222  }
223
224
225@enum.unique
226class MediumUpgradeType(enum.IntEnum):
227  DEFAULT = 0
228  DISRUPTIVE = 1
229  NON_DISRUPTIVE = 2
230
231
232@enum.unique
233class WifiD2DType(enum.IntEnum):
234  SCC_2G = 0
235  SCC_5G = 1
236  MCC_2G_WFD_5G_STA = 2
237  MCC_2G_WFD_5G_INDOOR_STA = 3
238  MCC_5G_WFD_5G_DFS_STA = 4
239  MCC_5G_HS_5G_DFS_STA = 5
240
241
242@enum.unique
243class SingleTestFailureReason(enum.IntEnum):
244  """The failure reasons for a nearby connect connection test."""
245  UNINITIALIZED = 0
246  SOURCE_START_DISCOVERY = 1
247  TARGET_START_ADVERTISING = 2
248  SOURCE_REQUEST_CONNECTION = 3
249  TARGET_ACCEPT_CONNECTION = 4
250  WIFI_MEDIUM_UPGRADE = 5
251  FILE_TRANSFER_FAIL = 6
252  FILE_TRANSFER_THROUGHPUT_LOW = 7
253  SOURCE_WIFI_CONNECTION = 8
254  TARGET_WIFI_CONNECTION = 9
255  AP_IS_NOT_CONFIGURED = 10
256  DISCONNECTED_FROM_AP = 11
257  WRONG_AP_FREQUENCY = 12
258  WRONG_P2P_FREQUENCY = 13
259  DEVICE_CONFIG_ERROR = 14
260  SUCCESS = 15
261
262
263COMMON_WIFI_CONNECTION_FAILURE_REASONS = (
264    ' 1) Check if the wifi ssid or password is correct;\n',
265    ' 2) Try to remove any saved wifi network from wifi settings;\n',
266    ' 3) Check if other device can connect to the same AP\n',
267    ' 4) Check the wifi connection related log on the device.\n',
268)
269
270COMMON_TRIAGE_TIP: dict[SingleTestFailureReason, str] = {
271    SingleTestFailureReason.UNINITIALIZED: (
272        'not executed, the whole test was exited earlier; the devices may be'
273        ' disconnected from the host, abnormal things, such as system crash, '
274        ' mobly snippet was killed; Or something wrong with the script, check'
275        ' the test running log and the corresponding bugreport log.'
276    ),
277    SingleTestFailureReason.SUCCESS: 'success!',
278    SingleTestFailureReason.SOURCE_START_DISCOVERY: (
279        'The source device fails to discover the target device.'
280    ),
281    SingleTestFailureReason.TARGET_START_ADVERTISING: (
282        'The target device can not start advertising.'
283    ),
284    SingleTestFailureReason.SOURCE_REQUEST_CONNECTION: (
285        'The source device fails to connect to the target device'
286    ),
287    SingleTestFailureReason.TARGET_ACCEPT_CONNECTION: (
288        'The target device fails to accept the connection.'
289    ),
290    SingleTestFailureReason.SOURCE_WIFI_CONNECTION: (
291        'The source device can not connect to the wifi AP.\n'
292        f'{COMMON_WIFI_CONNECTION_FAILURE_REASONS}'
293    ),
294    SingleTestFailureReason.TARGET_WIFI_CONNECTION: (
295        'The target device can not connect to the wifi AP.\n'
296        f'{COMMON_WIFI_CONNECTION_FAILURE_REASONS}'
297    ),
298    SingleTestFailureReason.AP_IS_NOT_CONFIGURED: (
299        'The test AP is not set correctly in the test configuration file.'
300    ),
301    SingleTestFailureReason.DISCONNECTED_FROM_AP: (
302        'The STA is disconnected from the AP. Check AP DHCP config. Check if'
303        ' other devices can connect to the same AP.'
304    ),
305    SingleTestFailureReason.WRONG_AP_FREQUENCY: (
306        'Check if the test AP is set to the expected frequency.'
307    ),
308    SingleTestFailureReason.WRONG_P2P_FREQUENCY: '\n'.join([
309        'The test P2P frequency is not set to the expected value.',
310        ' Check if device capabilities are set correctly in the config file.',
311        ' If it is SCC DBS test case, check if the device does support DBS;',
312        (
313            ' If it is the SCC indoor or DFS test case, check if the device'
314            ' does support indoor/DFS channels in WFD mode;'
315        ),
316        (
317            ' If it is a MCC test, check if devices actually supports DBS,'
318            ' indoor or DFS feature  and set device capabilities correctly.'
319        ),
320    ]),
321    SingleTestFailureReason.DEVICE_CONFIG_ERROR: (
322        'Check if device capabilities are set correctly in the config file.'
323    ),
324}
325
326COMMON_WFD_UPGRADE_FAILURE_REASONS = '\n'.join([
327    'If WFD GO fails to start, check your factory build to ensure that',
328    (
329        ' 1) includes the wpa_supplicant patch to avoid scan before starting GO'
330        ' https://w1.fi/cgit/hostap/commit/?id=b18d95759375834b6ca6f864c898f27d161b14ca.'
331    ),
332    (
333        ' 2) includes WiFi mainline module 34.11.10.06.0 or later version which'
334        ' fixes the out-of-order message issue between P2P and tethering'
335        ' modules'
336    ),
337    (
338        ' 3) HAL getUsableChannels() returns the correct channel list. Run "adb'
339        ' shell cmd wifi get-allowed-channel" and ensure it does not include'
340        ' DFS channels unless config_wifiEnableStaDfsChannelForPeerNetwork is'
341        ' set to true. DFS channels can be found from'
342        ' https://en.wikipedia.org/wiki/List_of_WLAN_channels.'
343    ),
344    (
345        'Also check if BT socket is still connected and read/write is normal'
346        ' when the upgrade failure happens'
347    ),
348])
349
350MEDIUM_UPGRADE_FAIL_TRIAGE_TIPS: dict[NearbyMedium, str] = {
351    NearbyMedium.WIFILAN_ONLY: (
352        ' WLAN, check if AP blocks the mDNS traffic. Check if STA is connected'
353        ' to AP during WiFi upgrade.'
354    ),
355    NearbyMedium.UPGRADE_TO_WIFIHOTSPOT: (
356        ' HOTSPOT, check the related wifip2p and NearbyConnections logs to see'
357        ' if the WFD group owner fails to start on'
358        ' the target side or the STA fails to connect on the source side.\n'
359        f' {COMMON_WFD_UPGRADE_FAILURE_REASONS}'
360    ),
361    NearbyMedium.UPGRADE_TO_WIFIDIRECT: (
362        ' WFD, check the related wifip2p and NearbyConnections logs if the WFD'
363        ' group owner fails to start on the target side or WFD group client'
364        ' fails to connect on the source side. \n'
365        f' {COMMON_WFD_UPGRADE_FAILURE_REASONS}'
366    ),
367    NearbyMedium.UPGRADE_TO_ALL_WIFI: (
368        ' all WiFI mediums, check NearbyConnections logs to see if WFD, WLAN'
369        ' and HOTSPOT mediums are tried and if the failure is on the target or'
370        ' source side. Check directed test results to see which medium fails.'
371    ),
372}
373
374
375@dataclasses.dataclass(frozen=True)
376class ConnectionSetupTimeouts:
377  """The timeouts of the nearby connection setup."""
378  discovery_timeout: datetime.timedelta | None = None
379  connection_init_timeout: datetime.timedelta | None = None
380  connection_result_timeout: datetime.timedelta | None = None
381
382
383@dataclasses.dataclass(frozen=False)
384class ConnectionSetupQualityInfo:
385  """The quality information of the nearby connection setup."""
386  discovery_latency: datetime.timedelta = UNSET_LATENCY
387  connection_latency: datetime.timedelta = UNSET_LATENCY
388  medium_upgrade_latency: datetime.timedelta = UNSET_LATENCY
389  medium_upgrade_expected: bool = False
390  upgrade_medium: NearbyConnectionMedium | None = None
391  medium_frequency: int = INVALID_INT
392
393  def get_dict(self) -> dict[str, str]:
394    dict_repr = {
395        'discovery': f'{round(self.discovery_latency.total_seconds(), 1)}s',
396        'connection': f'{round(self.connection_latency.total_seconds(), 1)}s'
397    }
398    if self.medium_upgrade_expected:
399      dict_repr['upgrade'] = (
400          f'{round(self.medium_upgrade_latency.total_seconds(), 1)}s'
401      )
402    if self.upgrade_medium:
403      dict_repr['medium'] = self.upgrade_medium.name
404    return dict_repr
405
406  def get_medium_name(self) -> str:
407    if self.upgrade_medium:
408      return self.upgrade_medium.name
409    return 'na'
410
411
412@dataclasses.dataclass(frozen=False)
413class SingleTestResult:
414  """The test result of a single iteration."""
415
416  test_iteration: int = 0
417  is_failed_with_prior_bt: bool = False
418  failure_reason: SingleTestFailureReason = (
419      SingleTestFailureReason.UNINITIALIZED
420  )
421  result_message: str = ''
422  prior_nc_quality_info: ConnectionSetupQualityInfo = dataclasses.field(
423      default_factory=ConnectionSetupQualityInfo
424  )
425  discoverer_sta_latency: datetime.timedelta = UNSET_LATENCY
426  quality_info: ConnectionSetupQualityInfo = (
427      dataclasses.field(default_factory=ConnectionSetupQualityInfo)
428  )
429  file_transfer_throughput_kbps: float = UNSET_THROUGHPUT_KBPS
430  iperf_throughput_kbps: float = UNSET_THROUGHPUT_KBPS
431  advertiser_sta_latency: datetime.timedelta = UNSET_LATENCY
432  discoverer_sta_expected: bool = False
433  advertiser_wifi_expected: bool = False
434  sta_frequency: int = INVALID_INT
435  max_sta_link_speed_mbps: int = INVALID_INT
436  start_time: datetime.datetime = datetime.datetime.now()
437
438
439@dataclasses.dataclass(frozen=False)
440class NcPerformanceTestMetrics:
441  """Metrics data for quick start test."""
442
443  prior_bt_discovery_latencies: list[datetime.timedelta] = dataclasses.field(
444      default_factory=list[datetime.timedelta]
445  )
446  prior_bt_connection_latencies: list[datetime.timedelta] = dataclasses.field(
447      default_factory=list[datetime.timedelta]
448  )
449  discoverer_wifi_sta_latencies: list[datetime.timedelta] = dataclasses.field(
450      default_factory=list[datetime.timedelta]
451  )
452  file_transfer_discovery_latencies: list[datetime.timedelta] = (
453      dataclasses.field(default_factory=list[datetime.timedelta])
454  )
455  file_transfer_connection_latencies: list[datetime.timedelta] = (
456      dataclasses.field(default_factory=list[datetime.timedelta])
457  )
458  medium_upgrade_latencies: list[datetime.timedelta] = dataclasses.field(
459      default_factory=list[datetime.timedelta])
460  advertiser_wifi_sta_latencies: list[datetime.timedelta] = dataclasses.field(
461      default_factory=list[datetime.timedelta])
462  file_transfer_throughputs_kbps: list[float] = dataclasses.field(
463      default_factory=list[float])
464  iperf_throughputs_kbps: list[float] = dataclasses.field(
465      default_factory=list[float])
466  upgraded_wifi_transfer_mediums: list[NearbyConnectionMedium] = (
467      dataclasses.field(default_factory=list[NearbyConnectionMedium]))
468
469
470@dataclasses.dataclass(frozen=True)
471class TestResultStats:
472  """The test result stats."""
473  success_count: int | None = None
474  min_val: float | None = None
475  median_val: float | None = None
476  max_val: float | None = None
477