mirror of
https://github.com/espressif/openthread.git
synced 2026-08-12 21:57:47 +00:00
This commit refactors the cp-caps test as follows: 1. uses the unittest framework to refactor the cp-caps. 2. all tests are sorted alphabetically 3. all test names are changed to long names.
228 lines
9.1 KiB
Python
228 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (c) 2025, The OpenThread Authors.
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
# 1. Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# 2. Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
# 3. Neither the name of the copyright holder nor the
|
|
# names of its contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
#
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
import threading
|
|
import queue
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
import otci
|
|
from otci import OTCI
|
|
from otci.errors import ExpectLineTimeoutError, InvalidArgumentsError
|
|
|
|
CP_CAPABILITY_VERSION = "0.1.1-dev"
|
|
|
|
logging.basicConfig(level=logging.WARNING)
|
|
|
|
|
|
@dataclass
|
|
class Frame:
|
|
"""Represents a Thread radio frame.
|
|
|
|
Attributes:
|
|
name: The description of the frame.
|
|
tx_frame: The psdu of the frame that is to be sent.
|
|
dst_address: The destination MAC address of the `tx_frame`. It is used by the receiver to filter
|
|
out the `tx_frame`.
|
|
is_security_processed: The value of the `otRadioFrame.mInfo.mTxInfo.mIsSecurityProcessed` field.
|
|
If it is set to False, the `active_dataset` and `src_ext_address` should also be set for the
|
|
radio driver to encrypt the `tx_frame`.
|
|
expect_rx_frame: The frame expected to be received. The expected frame should be the same as the
|
|
`tx_frame` if the `expect_rx_frame` is set to None.
|
|
active_dataset: The active dataset.
|
|
src_ext_address: The source extended MAC address of the transmitter.
|
|
"""
|
|
name: str
|
|
tx_frame: str
|
|
dst_address: str
|
|
is_security_processed: Optional[bool] = True
|
|
expect_rx_frame: Optional[str] = None
|
|
active_dataset: Optional[str] = None
|
|
src_ext_address: Optional[str] = None
|
|
|
|
|
|
class DeviceManager(object):
|
|
"""Provides the DUT, reference device and common methods for test cases."""
|
|
|
|
DEFAULT_FORMAT_ALIGN_LENGTH = 58 # The default formatted string alignment length
|
|
|
|
def __init__(self):
|
|
self.__dut = self.__connect_dut()
|
|
self.__ref = self.__connect_reference_device()
|
|
|
|
if self.__get_debug_enabled():
|
|
logger = logging.getLogger()
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
@property
|
|
def dut(self) -> OTCI:
|
|
return self.__dut
|
|
|
|
@property
|
|
def ref(self) -> OTCI:
|
|
return self.__ref
|
|
|
|
def output_test_result_string(self, name: str, value: str, align_length: int = DEFAULT_FORMAT_ALIGN_LENGTH):
|
|
prefix = (name + ' ').ljust(align_length, '-')
|
|
print(f'{prefix} {value}')
|
|
|
|
def output_test_result_bool(self, name: str, value: bool, align_length: int = DEFAULT_FORMAT_ALIGN_LENGTH):
|
|
self.output_test_result_string(name, 'OK' if value else 'NotSupported', align_length)
|
|
|
|
def get_default_dataset(self):
|
|
return self.__dut.create_dataset(channel=20, network_key='00112233445566778899aabbccddcafe')
|
|
|
|
def send_formatted_frames_retries(self, sender: OTCI, receiver: OTCI, frame: Frame, max_send_retries: int = 5):
|
|
for i in range(0, max_send_retries):
|
|
if self.__send_formatted_frame(sender, receiver, frame):
|
|
return True
|
|
|
|
return False
|
|
|
|
#
|
|
# Private methods
|
|
#
|
|
def __get_debug_enabled(self) -> bool:
|
|
enabled = os.getenv('DEBUG', None)
|
|
return enabled == 'on'
|
|
|
|
def __get_adb_key(self) -> Optional[str]:
|
|
return os.getenv('ADB_KEY', None)
|
|
|
|
def __connect_dut(self) -> OTCI:
|
|
if os.getenv('DUT_ADB_TCP'):
|
|
node = otci.connect_otbr_adb_tcp(os.getenv('DUT_ADB_TCP'), adb_key=self.__get_adb_key())
|
|
elif os.getenv('DUT_ADB_USB'):
|
|
node = otci.connect_otbr_adb_usb(os.getenv('DUT_ADB_USB'), adb_key=self.__get_adb_key())
|
|
elif os.getenv('DUT_CLI_SERIAL'):
|
|
node = otci.connect_cli_serial(os.getenv('DUT_CLI_SERIAL'))
|
|
elif os.getenv('DUT_SSH'):
|
|
node = otci.connect_otbr_ssh(os.getenv('DUT_SSH'))
|
|
else:
|
|
raise InvalidArgumentsError(
|
|
"Please set DUT_ADB_TCP, DUT_ADB_USB, DUT_CLI_SERIAL or DUT_SSH to connect to the DUT device.")
|
|
|
|
return node
|
|
|
|
def __connect_reference_device(self) -> OTCI:
|
|
if os.getenv('REF_CLI_SERIAL'):
|
|
node = otci.connect_cli_serial(os.getenv('REF_CLI_SERIAL'))
|
|
elif os.getenv('REF_SSH'):
|
|
node = otci.connect_otbr_ssh(os.getenv('REF_SSH'))
|
|
elif os.getenv('REF_ADB_USB'):
|
|
node = otci.connect_otbr_adb_usb(os.getenv('REF_ADB_USB'), adb_key=self.__get_adb_key())
|
|
else:
|
|
raise InvalidArgumentsError(
|
|
"Please set REF_CLI_SERIAL, REF_SSH or REF_ADB_USB to connect to the reference device.")
|
|
|
|
return node
|
|
|
|
def __send_formatted_frame(self, sender: OTCI, receiver: OTCI, frame: Frame):
|
|
# When `is_security_processed` is False, the frame may need the radio driver
|
|
# to encrypt the frame. Here sets the active dataset and the MAC source address for the
|
|
# radio driver to encrypt the frame.
|
|
if frame.is_security_processed is False:
|
|
if frame.active_dataset is None or frame.src_ext_address is None:
|
|
raise InvalidArgumentsError(
|
|
"When the 'is_security_processed' is 'False', the 'active_dataset' and 'src_ext_address' must be set"
|
|
)
|
|
# Do the factory reset to set the frame counter to 0.
|
|
sender.factory_reset()
|
|
receiver.factory_reset()
|
|
sender.set_dataset_bytes('active', frame.active_dataset)
|
|
sender.set_extaddr(frame.src_ext_address)
|
|
|
|
sender.diag_start()
|
|
receiver.diag_start()
|
|
|
|
channel = 11
|
|
num_sent_frames = 1
|
|
|
|
sender.diag_set_channel(channel)
|
|
receiver.diag_set_channel(channel)
|
|
receiver.diag_radio_receive()
|
|
receiver.diag_set_radio_receive_filter_dest_mac_address(frame.dst_address)
|
|
receiver.diag_enable_radio_receive_filter()
|
|
|
|
result_queue = queue.Queue()
|
|
receive_task = threading.Thread(target=self.__radio_receive_task,
|
|
args=(receiver, num_sent_frames, result_queue),
|
|
daemon=True)
|
|
receive_task.start()
|
|
|
|
sender.wait(0.1)
|
|
|
|
sender.diag_frame(frame.tx_frame,
|
|
is_security_processed=frame.is_security_processed,
|
|
max_csma_backoffs=4,
|
|
max_frame_retries=4,
|
|
csma_ca_enabled=True)
|
|
sender.diag_send(num_sent_frames, is_async=False)
|
|
|
|
receive_task.join()
|
|
|
|
if result_queue.empty():
|
|
ret = False # No frame is received.
|
|
else:
|
|
received_frames = result_queue.get()
|
|
if len(received_frames) != num_sent_frames:
|
|
ret = False
|
|
else:
|
|
# The radio driver may not append the FCF field to the received frame. Do not check the FCF field here.
|
|
FCF_LENGTH = 4
|
|
expect_frame = frame.expect_rx_frame or frame.tx_frame
|
|
ret = expect_frame[:-FCF_LENGTH] == received_frames[0]['psdu'][:-FCF_LENGTH]
|
|
|
|
if ret:
|
|
sender.diag_stop()
|
|
receiver.diag_stop()
|
|
else:
|
|
# The command `diag radio receive <number>` may fail to receive specified number of frames in default
|
|
# timeout time. In this case, the diag module still in `sync` mode, and it only allows users to run the
|
|
# command `factoryreset` to terminate the test.
|
|
sender.factory_reset()
|
|
receiver.factory_reset()
|
|
|
|
return ret
|
|
|
|
def __radio_receive_task(self, receiver: OTCI, number: int, result_queue: queue):
|
|
try:
|
|
receiver.set_execute_command_retry(0)
|
|
result = receiver.diag_radio_receive_number(number)
|
|
except ExpectLineTimeoutError:
|
|
pass
|
|
else:
|
|
result_queue.put(result)
|
|
finally:
|
|
receiver.set_execute_command_retry(OTCI.DEFAULT_EXEC_COMMAND_RETRY)
|