- Plan the BTP interface
- Get familiar with tools
- Add a new profile
- Add a new test case
- Synchronize multiple Lower Testers
- Keep global variables in Stack
PTS running in automation mode sends MMI requests to AutoPTS which handles them by using BTP protocol. Each PTS profile has its own set of MMI messages that can be found in ATS files. The links to ATS files can be accessed in PTS GUI -> Start Page -> Reference Documents.
Reviewing MMI messages of a profile allows to gain a bigger picture of how the BTP interface for that profile should be designed.
Planning the BTP interface before starting the implementation process will:
- avoid IUT specific implementation,
- make the BTP interface more generic in terms of reusing across multiple profiles, reducing the amount of code on the IUT tester side,
- reduce the number of frequent BTP changes, so multiple projects could enjoy the AutoPTS without disturbing their Bluetooth product development process.
Adding support for all test cases of a profile at once should result with a complete BTP interface for the profile. If a partial BTP interface is being contributed, with a few BTP commands, then those commands should be well-thought-out so that they will not need to be updated in the future to complete the remaining test cases.
Changing the parameters of a BTP command/response/event that has been used for a while is difficult, because it might already have been adopted by many and the change may rise objections. Adding a new helper command to the completed BTP interfaces is more acceptable as it does not affect the existing IUT tester implementations.
There are few log sources that will be helpful:
- PTS and Bluetooth Protocol Viewer logs,
- AutoPTS client and server logs,
- Jlink RTT logs,
- Btmon logs,
- Ellisys logs.
The logs allow to peek what happens on the Host side of the PTS dongle during a test case.
AutoPTS saves the logs in the directory of the PTS workspace (the .pqw6 file). Inside a test case sub folder there should be 4 files:
.xml
- PTS debug logs. Useful to find out what was the reason of a test case failure when a problem was related to some failing checks in case some test case conditions were not met..cfa
,.frm
and.fsc
- can be open only with BPV. They allow you to view parsed packets exchanged between the host and the controller of the PTS dongle.
The logs allow to peek what happens on the AutoPTS client or server side:
- what MMI are requested by PTS,
- what response to MMI is sent back to PTS,
- all data and order of the BTP communication,
- AutoPTS framework python exceptions, including missing MMI/WID handlers, WID implementation bugs, bot features exceptions.
AutoPTS creates a log file named after the autopts client launcher that is used
e.g. autoptsclient_zephyr_65000.log
, autoptsclient_bot_65000_65002.log
,
autoptsserver_65000_65002.log
... These log files contains AutoPTS client logs
from all test cases in the run. For convenience, the client logs are saved one
file per test case too in the logs/
folder.
If an IUT board supports debug logs sent via Jlink RTT logs, AutoPTS can read
them during a test case and save them in the logs/
folder.
If an IUT supports btmon logs, AutoPTS can read them during a test case and
save them in the logs/
folder in a btsnoop or plain text format.
If you are lucky user of Ellisys sniffer you will be able to resolve the tricky cases, where a controller is at fault.
To add a new profile to the AutoPTS, we have to start with some boilerplate code.
This can be done with the ./tools/generate_profile.py
generator. An example usage
that creates profile named PROFILE for the zephyr
project:
$ python ./tools/generate_profile.py
Enter project name: zephyr
Enter profile name: profile
Enter new BTP service ID (max 255): 26
The generator creates all required new files and appends some additional imports and entries to the existing ones to tie everything together. Let's do a quick overview of what the generated files and entries are for:
This file is for a description of the profile BTP interface.
This file is for implementations of the BTP interface, i.e. functions for packing the BTP commands, and parsers of the BTP events and responses.
New BTP service interface needs an ID, so create a new entry BTP_SERVICE_ID_PROFILE = 26
inside the autopts/pybtp/defs.py
, where the value should be the next available ID value.
Keep all BTP interface opcodes inside the file, i.e. all the BTP commands and events opcodes.
This file is for MMI/WID handlers of the profile, generic for all projects. The handlers
make use of the BTP commands, responses and events. If there is a need for a project specific
handler, then it should be added into the autopts/ptsprojects/zephyr/profile_wid.py
inside
the project.
The class Stack
is a good place for global variables that have to be shared
between MMI/WID handlers, because the Stack instance is reinitialized before
start of each test case. So create a class like class Profile
and make it a part
of the Stack.
This file has to contain two functions:
def set_pixits(ptses)
and def test_cases(ptses)
.
The def set_pixits(ptses)
configures how AutoPTS should overwrite the default
PIXITs value of a PTS workspace. The function could be as short as:
def set_pixits(ptses):
pts = ptses[0]
pts.set_pixit("BAP", "TSPX_time_guard", "180000")
pts.set_pixit("BAP", "TSPX_use_implicit_send", "TRUE")
if len(ptses) < 2:
return
pts2 = ptses[1]
pts2.set_pixit("BAP", "TSPX_time_guard", "180000")
pts2.set_pixit("BAP", "TSPX_use_implicit_send", "TRUE")
The def test_cases(ptses)
is used to customize the IUT configuration and behavior
before and after running test cases of the profile. A short version of the function
could look like this:
def test_cases(ptses):
"""Returns a list of PROFILE test cases
ptses -- list of PyPTS instances"""
pts = ptses[0]
stack = get_stack()
# Generic preconditions for all test case in the profile
pre_conditions = [
TestFunc(btp.core_reg_svc_profile),
TestFunc(stack.profile_init)
]
test_case_name_list = pts.get_test_case_list('PROFILE')
tc_list = []
# Use the same preconditions and MMI/WID handler for all test cases of the profile
for tc_name in test_case_name_list:
instance = ZTestCase('PROFILE', tc_name, cmds=pre_conditions,
generic_wid_hdl=profile_wid_hdl)
tc_list.append(instance)
return tc_list
In almost every profile there are multiple test cases that need a custom preconditions, e.g. at the start of a test case PTS expects a specific advertisement in the air, but there is no MMI (WID) for it. Test cases with multiple Lower Testers (PTS instances) usually need to be synchronized with Synch Points to avoid collision of BTP commands, so those can be customized here too.
The profile_wid_hdl
parameter of the TestCase class is a generic handler that
will find a right handler for an MMI (WID) request received from Lower Tester
(PTS instance). In most test cases a project will use handlers only from
autopts/wid
, but sometimes a custom, project-specific WID is required.
It is convenient to create a separate file in your project for this, e.g:
autopts/ptsprojects/zephyr/profile_wid.py
:
# profile_wid.py file
import logging
from autopts.wid import generic_wid_hdl
from autopts.pybtp.types import WIDParams
log = logging.debug
def profile_wid_hdl(wid, description, test_case_name):
log(f'{profile_wid_hdl.__name__}, {wid}, {description}, {test_case_name}')
# Try to find the WID handler in this file, if not found then proceed to
# the generic autopts/wid/profile.py.
return generic_wid_hdl(wid, description, test_case_name,
[__name__, 'autopts.wid.profile'])
def hdl_wid_104(_: WIDParams):
"""
Confirm if the Lower Tester received ISO stream.
"""
# Should be handled by the Lower Tester, errata in progress.
return True
Assuming you have completed the setup of the profile, you are ready to start implementing BTP interface and missing MMI/WID handlers for a test case.
Depending on the role the IUT is given in the test case, the IUT might have to
start scanning or advertising. If an IUT should act as a client, the PTS can
request a MMI similar to 20100: "Please initiate a GATT connection to the PTS."
It means that the IUT should start scanning and then proceed with establishing
a Link Layer connection with the PTS dongle. This behavior is covered by BTP command
of GAP BTP Service:
Opcode 0x0e - Connect command/responsecan
defined in doc/btp_gap.txt
and implemented
in autopts/pybtp/btp/gap.py
. So the handler of MMI 20100 is implemented as fallow:
def hdl_wid_20100(_: WIDParams):
btp.gap_conn()
return True
For test cases with the IUT in a server role, it depends on the test case if the PTS will ask you with an MMI to start advertising, but if it does not, then you have to start the action on your own.
This can be scripted in def test_cases(ptses)
. If this action is used
in just a few test cases, you can hardcord them in custom_test_cases
list,
but if it applies to most of the profile test cases, then just use pre_conditions
to skip listing a hundred of test case names.
def test_cases(ptses):
"""Returns a list of PROFILE test cases
ptses -- list of PyPTS instances"""
pts = ptses[0]
stack = get_stack()
# Generic preconditions for all test case in the profile
pre_conditions = [
TestFunc(btp.core_reg_svc_profile),
TestFunc(stack.profile_init)
]
# Custom preconditions
pre_conditions_server = pre_conditions + [
TestFunc(btp.gap_set_extended_advertising_on),
TestFunc(lambda: btp.gap_adv_ind_on(ad=ad)),
]
test_case_name_list = pts.get_test_case_list('PROFILE')
tc_list = []
ad = {
AdType.name_full: iut_device_name[::1].hex(),
AdType.flags: format(AdFlags.br_edr_not_supp |
AdFlags.le_gen_discov_mode, '02x'),
}
# Use custom preconditions
custom_test_cases = [
ZTestCase("PROFILE", "PROFILE/SR/ADV/BV-01-C",
cmds=pre_conditions_server,
generic_wid_hdl=profile_wid_hdl)
]
for tc_name in test_case_name_list:
instance = ZTestCase('PROFILE', tc_name, cmds=pre_conditions,
generic_wid_hdl=profile_wid_hdl)
# Use custom preconditions
for custom_tc in custom_test_cases:
if tc_name == custom_tc.name:
instance = custom_tc
break
tc_list.append(instance)
return tc_list
In test cases with multiple Lower Testers it is common to synchronize WID execution to avoid BTP commands collision or interrupting a busy IUT. This can be handled with Synch Points mechanism.
# In test_cases(ptses) function:
# Use custom preconditions
custom_test_cases = [
ZTestCase("BAP", "BAP/BA/BASS/BV-04-C", cmds=pre_conditions +
[ # Barrier no. 1:
# LT1 will not execute the WID 20100 until LT2 receives
# the WID 100 request. LT2 will not execute WID 100
# until LT1 completes its WID 20100.
TestFunc(get_stack().synch.add_synch_element,
[SynchPoint("BAP/BA/BASS/BV-04-C", 20100),
SynchPoint("BAP/BA/BASS/BV-04-C_LT2", 100)]),
# Barrier no. 2:
# LT2 will not execute the WID 384 until LT1 receives
# the WID 345 request. LT1 will not execute WID 345
# until LT2 completes its WID 384.
TestFunc(get_stack().synch.add_synch_element,
[SynchPoint("BAP/BA/BASS/BV-04-C_LT2", 384),
SynchPoint("BAP/BA/BASS/BV-04-C", 345)])],
generic_wid_hdl=bap_wid_hdl,
lt2="BAP/BA/BASS/BV-04-C_LT2"),
]
test_cases_lt2 = [
ZTestCaseSlave("BAP", "BAP/BA/BASS/BV-04-C_LT2",
cmds=pre_conditions_lt2,
generic_wid_hdl=bap_wid_hdl),
]