Files
Yakun Xu 1f6c3ad433 [tools] support host POSIX ot-cli and adb spinel proxy in otci and cp-caps (#13791)
Support running cp-caps and OTCI using a host POSIX ot-cli instance
connected to a Radio URL (e.g. spinel+hdlc+unix://) or via automated
Android adb Spinel proxy port forwarding.

This enables running the latest host-built OpenThread diagnostics and
capability tests against co-processors without relying on on-device
ot-ctl or older Android system binaries.
2026-10-27 17:29:15 +01:00
..

Co-Processor Capability Test Suite (cp-caps)

The Co-Processor Capability Test Suite (cp-caps) checks which capabilities an OpenThread co-processor (currently an RCP) supports, such as diag commands, IEEE 802.15.4 frame formats, CSL, data poll, and link metrics. It also measures performance metrics, such as Thread network 1-hop throughput.

Unlike a conformance or certification test suite, it profiles the co-processor: each test case reports whether a capability is supported (OK or NotSupported) or a measured value, rather than an overall pass or fail.

The test suite consists of:

  • Test cases (test_*.py): each test case tests a capability area and is run using Python's unittest framework.
  • A test harness (device_manager.py): connects to and controls the testbed devices using OTCI and outputs the results.
  • A testbed: the devices the test cases run on (see Testbed).

Testbed

                    +-------+
    +---------------|  PC   |----------------+
    |               +-------+                |
    | ADB/SSH           |                    | ADB/SSH/SERIAL
    |                   | SERIAL             |
+-------+               |           +------------------+
|  DUT  |<-----------Thread-------->| Reference Device |
+-------+               |           +------------------+
                  +-----------+
                  |  Sniffer  | (optional)
                  +-----------+
  • PC : The computer to run the test script.
  • DUT : The device under test.
  • Reference Device : The device that supports all tested features.
  • Sniffer : An RCP that captures frames with timestamps (e.g. to check the timing of frames). It is optional and only used by some test cases.

Reference Device

The nRF52840DK is set as the reference device by default. Testers can also select the other Thread device as the reference device.

Sniffer

The sniffer is an RCP (e.g. an nRF52840 dongle running ot-rcp) driven by ot-extcap from ot-br-posix. The test cases capture frames by running ot-extcap on the channel of interest, and read each frame with its timestamp in microseconds taken by the sniffer radio.

Quick guide to building ot-extcap:

$ git clone [email protected]:openthread/ot-br-posix.git
$ cd ot-br-posix/
$ git submodule update --init
$ ./script/cmake-build extcap

The executable is at build/extcap/ot-extcap. Set OT_EXTCAP to its full path (unless it is in the PATH), and SNIFFER_SERIAL (or SNIFFER_RADIO_URL) to the sniffer RCP.

Quick guide to setting up the nRF52840DK:

$ git clone [email protected]:openthread/ot-nrf528xx.git
$ cd ot-nrf528xx/
$ git submodule update --init
$ ./script/bootstrap
$ ./script/build nrf52840 UART_trans -DOT_DIAGNOSTIC=ON -DOT_CSL_RECEIVER=ON -DOT_LINK_METRICS_INITIATOR=ON -DOT_LINK_METRICS_SUBJECT=ON -DOT_WAKE_INITIATOR=ON
$ arm-none-eabi-objcopy -O ihex build/bin/ot-cli-ftd ot-cli-ftd.hex
$ nrfutil device program --firmware ot-cli-ftd.hex --options chip_erase_mode=ERASE_ALL
$ nrfutil device reset --reset-kind=RESET_PIN

Python Virtual Environment Setup

It is recommended to use a Python virtual environment to manage project dependencies and avoid conflicts with system-wide packages.

Creation

Create a virtual environment named .venv in the project directory:

$ cd openthread
$ python3 -m venv .venv

Activation

Activate the virtual environment using the following command:

$ source .venv/bin/activate

After activation, your shell prompt should indicate that you are working within the virtual environment.

Dependency Installation

Once the virtual environment is activated, install the required dependencies using pip:

$ pip3 install -r ./tools/cp-caps/requirements.txt ./tools/otci

Deactivation

When you are finished working in the virtual environment, you can deactivate it by running the following command. This will return you to your system's global Python environment.

$ deactivate

Test Commands

Help Info

usage: [device configurations] python3 -m unittest -q [test cases]

This test suite is used for testing the capabilities of the RCP.

Device configurations:
  ADB_KEY=<adb_key>                  Full path to the adb key
  DEBUG=on                           Enable the debug information
  DUT_ADB_TCP=<device_ip>            Connect to the DUT via adb tcp
  DUT_ADB_USB=<serial_number>        Connect to the DUT via adb usb
  DUT_ADB_SPINEL_PROXY=<serial>      Connect to the DUT Android device via adb Spinel proxy (host ot-cli)
  DUT_SPINEL_PROXY=1                 Enable adb Spinel proxy mode for DUT_ADB_USB/DUT_ADB_TCP (host ot-cli)
  DUT_SPINEL_PROXY_SOCKET=<path>     Socket path for adb Spinel proxy forwarding
  DUT_RADIO_URL=<radio_url>          Connect to the DUT via host POSIX ot-cli over radio URL
  DUT_CLI_SERIAL=<serial_device>     Connect to the DUT via cli serial port
  DUT_SSH=<device_ip>                Connect to the DUT via ssh
  REF_ADB_TCP=<device_ip>            Connect to the reference device via adb tcp
  REF_ADB_USB=<serial_number>        Connect to the reference device via adb usb
  REF_ADB_SPINEL_PROXY=<serial>      Connect to the reference Android device via adb Spinel proxy (host ot-cli)
  REF_SPINEL_PROXY=1                 Enable adb Spinel proxy mode for REF_ADB_USB/REF_ADB_TCP (host ot-cli)
  REF_SPINEL_PROXY_SOCKET=<path>     Socket path for reference device adb Spinel proxy forwarding
  REF_RADIO_URL=<radio_url>          Connect to the reference device via host POSIX ot-cli over radio URL
  REF_CLI_SERIAL=<serial_device>     Connect to the reference device via cli serial port
  REF_SSH=<device_ip>                Connect to the reference device via ssh
  OT_CLI=<ot_cli_path>               Full path to the host POSIX ot-cli executable (default: ot-cli)
  SNIFFER_SERIAL=<serial_device>     Use the RCP at the serial port as the sniffer (optional)
  SNIFFER_RADIO_URL=<radio_url>      Use the RCP at the radio URL as the sniffer (optional)
  OT_EXTCAP=<ot_extcap_path>         Full path to the ot-extcap executable used by the sniffer (default: ot-extcap)

Test cases:
  test_csl                 test whether the RCP supports CSL transmitter
  test_data_poll           test whether the RCP supports data poll
  test_diag_commands       test whether the RCP supports all diag commands
  test_frame_formats       test whether the RCP supports 802.15.4 frames of all formats
  test_link_metrics        test whether the RCP supports link metrics
  test_radio_frame_tx_info test mTxInfo field of the radio frame
  test_throughput          test Thread network 1-hop throughput

Examples:

Run test cases of the specified categories:
  DUT_ADB_USB=1169UC2F2T0M95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_csl test_data_poll

Run test cases with host POSIX ot-cli over Radio URL (e.g. Unix socket):
  DUT_RADIO_URL="spinel+hdlc+unix:///tmp/ot_spinel_proxy" REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_diag_commands

Run test cases with automated Android adb Spinel proxy setup:
  DUT_ADB_SPINEL_PROXY=1169UC2F2T0M95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_diag_commands

Run all test cases:
  DUT_ADB_USB=1169UC2F2T0M95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest discover -q -s ./ -p 'test_*.py'

Run specified test case:
  DUT_ADB_USB=1169UC2F2T0M95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_diag_commands.TestDiagCommands.test_diag_channel

These commands should be run in the path `tools/cp-caps`.

Note: If you get an error of LIBUSB_ERROR_BUSY when you are using the ADB USB interface, please run the command adb kill-server to kill the ADB server.

Test Diag Commands

The test case test_diag_commands tests all diag commands.

Following environment variables are used to configure diag command parameters:

  • DUT_DIAG_GPIO: Diag gpio value. The default value is 0 if it is not set.
  • DUT_DIAG_RAW_POWER_SETTING: Diag raw power setting value. The default value is 112233 if it is not set.
  • DUT_DIAG_POWER: Diag power value. The default value is 10 if it is not set.
tools/cp-caps$ DUT_ADB_USB=1269UCKFZTAM95OR REF_CLI_SERIAL=/dev/ttyACM0 DUT_DIAG_GPIO=2 DUT_DIAG_RAW_POWER_SETTING=44556688 DUT_DIAG_POWER=11 python3 -m unittest -q test_diag_commands
diag channel --------------------------------------------- OK
diag channel 20 ------------------------------------------ OK
diag cw start -------------------------------------------- OK
diag cw stop --------------------------------------------- OK
diag echo 0123456789 ------------------------------------- OK
diag echo -n 10 ------------------------------------------ OK
diag frame 00010203040506070809 -------------------------- OK
diag gpio mode 2 ----------------------------------------- OK
diag gpio mode 2 in -------------------------------------- OK
diag gpio mode 2 out ------------------------------------- OK
diag gpio get 2 ------------------------------------------ OK
diag gpio set 2 0 ---------------------------------------- OK
diag gpio set 2 1 ---------------------------------------- OK
diag power ----------------------------------------------- OK
diag power 11 -------------------------------------------- OK
diag powersettings --------------------------------------- NotSupported
diag powersettings 20 ------------------------------------ NotSupported
diag radio receive --------------------------------------- OK
diag radio sleep ----------------------------------------- OK
diag radio state ----------------------------------------- OK
diag rawpowersetting enable ------------------------------ NotSupported
diag rawpowersetting 44556688 ---------------------------- NotSupported
diag rawpowersetting ------------------------------------- NotSupported
diag rawpowersetting disable ----------------------------- NotSupported
diag repeat 10 64 ---------------------------------------- OK
diag repeat stop ----------------------------------------- OK
diag send 100 64 ----------------------------------------- OK
diag stats ----------------------------------------------- OK
diag stats clear ----------------------------------------- OK
diag stream start ---------------------------------------- NotSupported
diag stream stop ----------------------------------------- NotSupported

Test CSL Transmitter

The test case test_csl tests whether the RCP supports the CSL transmitter.

tools/cp-caps$ DUT_ADB_USB=TW69UCKFZTGM95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_csl
CSL Transmitter ------------------------------------------ OK

Test Data Poll

The test case test_data_poll tests whether the RCP supports data poll.

tools/cp-caps$ DUT_ADB_USB=TW69UCKFZTGM95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_data_poll
Data Poll Child ------------------------------------------ OK
Data Poll Parent ----------------------------------------- OK

The test case test_link_metrics tests whether the RCP supports link metrics.

tools/cp-caps$ DUT_ADB_USB=TW69UCKFZTGM95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_link_metrics
Link Metrics Initiator ----------------------------------- OK
Link Metrics Subject ------------------------------------- OK

Test Throughput

The test case test_throughput tests the Thread network 1-hop throughput of the DUT.

tools/cp-caps$ DUT_ADB_USB=TW69UCKFZTGM95OR REF_ADB_USB=44061HFAG01AQK python3 -m unittest -q test_throughput
Throughput ----------------------------------------------- 75.6 Kbits/sec

Test Frame Format

The test case test_frame_formats tests whether the RCP supports sending and receiving 802.15.4 frames of all formats.

tools/cp-caps$ DUT_ADB_USB=TW69UCKFZTGM95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_frame_formats
1 TX ver:2003,Cmd,seq,dst[addr:short,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 --------------- OK
1 RX ver:2003,Cmd,seq,dst[addr:short,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 --------------- OK
2 TX ver:2003,Bcon,seq,dst[addr:no,pan:no],src[addr:extd,pan:id],sec:no,ie:no,plen:30 -------------- OK
2 RX ver:2003,Bcon,seq,dst[addr:no,pan:no],src[addr:extd,pan:id],sec:no,ie:no,plen:30 -------------- OK
3 TX ver:2003,MP,noseq,dst[addr:extd,pan:id],src[addr:extd,pan:no],sec:l5,ie[ren con],plen:0 ------- OK
3 RX ver:2003,MP,noseq,dst[addr:extd,pan:id],src[addr:extd,pan:no],sec:l5,ie[ren con],plen:0 ------- OK
4 TX ver:2006,Cmd,seq,dst[addr:short,pan:id],src[addr:short,pan:no],sec:l5,ie:no,plen:0 ------------ OK
4 RX ver:2006,Cmd,seq,dst[addr:short,pan:id],src[addr:short,pan:no],sec:l5,ie:no,plen:0 ------------ OK
5 TX ver:2006,Cmd,seq,dst[addr:extd,pan:id],src[addr:extd,pan:no],sec:l5,ie:no,plen:0 -------------- OK
5 RX ver:2006,Cmd,seq,dst[addr:extd,pan:id],src[addr:extd,pan:no],sec:l5,ie:no,plen:0 -------------- OK
6 TX ver:2006,Data,seq,dst[addr:extd,pan:id],src[addr:extd,pan:id],sec:no,ie:no,plen:0 ------------- OK
6 RX ver:2006,Data,seq,dst[addr:extd,pan:id],src[addr:extd,pan:id],sec:no,ie:no,plen:0 ------------- OK
7 TX ver:2006,Data,seq,dst[addr:short,pan:id],src[addr:short,pan:id],sec:no,ie:no,plen:0 ----------- OK
7 RX ver:2006,Data,seq,dst[addr:short,pan:id],src[addr:short,pan:id],sec:no,ie:no,plen:0 ----------- OK
8 TX ver:2006,Data,seq,dst[addr:extd,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 --------------- OK
8 RX ver:2006,Data,seq,dst[addr:extd,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 --------------- OK
9 TX ver:2006,Data,seq,dst[addr:short,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 -------------- OK
9 RX ver:2006,Data,seq,dst[addr:short,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 -------------- OK
10 TX ver:2015,Data,seq,dst[addr:no,pan:no],src[addr:no,pan:no],sec:no,ie:no,plen:0 ---------------- OK
10 RX ver:2015,Data,seq,dst[addr:no,pan:no],src[addr:no,pan:no],sec:no,ie:no,plen:0 ---------------- OK
11 TX ver:2015,Data,seq,dst[addr:no,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 ---------------- OK
11 RX ver:2015,Data,seq,dst[addr:no,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 ---------------- OK
12 TX ver:2015,Data,seq,dst[addr:extd,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 -------------- OK
12 RX ver:2015,Data,seq,dst[addr:extd,pan:id],src[addr:no,pan:no],sec:no,ie:no,plen:0 -------------- OK
13 TX ver:2015,Data,seq,dst[addr:extd,pan:no],src[addr:no,pan:no],sec:no,ie:no,plen:0 -------------- OK
13 RX ver:2015,Data,seq,dst[addr:extd,pan:no],src[addr:no,pan:no],sec:no,ie:no,plen:0 -------------- OK
14 TX ver:2015,Data,seq,dst[addr:no,pan:no],src[addr:extd,pan:id],sec:no,ie:no,plen:0 -------------- OK
14 RX ver:2015,Data,seq,dst[addr:no,pan:no],src[addr:extd,pan:id],sec:no,ie:no,plen:0 -------------- OK
15 TX ver:2015,Data,seq,dst[addr:no,pan:no],src[addr:extd,pan:no],sec:no,ie:no,plen:0 -------------- OK
15 RX ver:2015,Data,seq,dst[addr:no,pan:no],src[addr:extd,pan:no],sec:no,ie:no,plen:0 -------------- OK
16 TX ver:2015,Data,seq,dst[addr:extd,pan:id],src[addr:extd,pan:no],sec:no,ie:no,plen:0 ------------ OK
16 RX ver:2015,Data,seq,dst[addr:extd,pan:id],src[addr:extd,pan:no],sec:no,ie:no,plen:0 ------------ OK
17 TX ver:2015,Data,seq,dst[addr:extd,pan:no],src[addr:extd,pan:no],sec:no,ie:no,plen:0 ------------ OK
17 RX ver:2015,Data,seq,dst[addr:extd,pan:no],src[addr:extd,pan:no],sec:no,ie:no,plen:0 ------------ OK
18 TX ver:2015,Data,seq,dst[addr:short,pan:id],src[addr:short,pan:id],sec:no,ie:no,plen:0 ---------- OK
18 RX ver:2015,Data,seq,dst[addr:short,pan:id],src[addr:short,pan:id],sec:no,ie:no,plen:0 ---------- OK
19 TX ver:2015,Data,seq,dst[addr:short,pan:id],src[addr:extd,pan:id],sec:no,ie:no,plen:0 ----------- OK
19 RX ver:2015,Data,seq,dst[addr:short,pan:id],src[addr:extd,pan:id],sec:no,ie:no,plen:0 ----------- OK
20 TX ver:2015,Data,seq,dst[addr:extd,pan:id],src[addr:short,pan:id],sec:no,ie:no,plen:0 ----------- OK
20 RX ver:2015,Data,seq,dst[addr:extd,pan:id],src[addr:short,pan:id],sec:no,ie:no,plen:0 ----------- OK
21 TX ver:2015,Data,seq,dst[addr:short,pan:id],src[addr:short,pan:id],sec:no,ie[csl],plen:0 -------- OK
21 RX ver:2015,Data,seq,dst[addr:short,pan:id],src[addr:short,pan:id],sec:no,ie[csl],plen:0 -------- OK
22 TX ver:2015,Data,noseq,dst[addr:short,pan:id],src[addr:short,pan:id],sec:no,ie:no,plen:0 -------- OK
22 RX ver:2015,Data,noseq,dst[addr:short,pan:id],src[addr:short,pan:id],sec:no,ie:no,plen:0 -------- OK
23 TX ver:2015,Data,seq,dst[addr:extd,pan:id],src[addr:extd,pan:no],sec:l5,keyid:2,ie:no,plen:0 ---- OK
23 RX ver:2015,Data,seq,dst[addr:extd,pan:id],src[addr:extd,pan:no],sec:l5,keyid:2,ie:no,plen:0 ---- OK

Test mTxInfo field of the radio frame.

The test case test_radio_frame_tx_info tests whether the RCP supports the mTxInfo field of the radio frame.

tools/cp-caps$ DUT_ADB_USB=TW69UCKFZTGM95OR REF_CLI_SERIAL=/dev/ttyACM0 python3 -m unittest -q test_radio_frame_tx_info
mCsmaCaEnabled=0 ----------------------------------------- OK
mCsmaCaEnabled=1 ----------------------------------------- OK
mIsSecurityProcessed=True -------------------------------- OK
mIsSecurityProcessed=False ------------------------------- OK
mMaxCsmaBackoffs=0 --------------------------------------- OK (12 ms)
mMaxCsmaBackoffs=100 ------------------------------------- OK (541 ms)
mRxChannelAfterTxDone ------------------------------------ OK
mTxDelayBaseTime=now,mTxDelay=500000 --------------------- OK