From ad74b1da17a7679476b2eba9212fe1780b6784b2 Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Thu, 15 Aug 2024 15:10:58 +0200 Subject: [PATCH 1/5] Bluetooth: CCP: Introduce new CCP API The CCP API for the Call Control Profile works on top of the TBS API, and will eventually replace parts of the TBS API. Signed-off-by: Emil Gydesen --- MAINTAINERS.yml | 2 + .../api/audio/bluetooth-le-audio-arch.rst | 7 +- .../bluetooth/bluetooth-shell.rst | 1 + .../bluetooth/shell/audio/ccp.rst | 235 ++-------------- .../bluetooth/shell/audio/tbs.rst | 237 ++++++++++++++++ include/zephyr/bluetooth/audio/ccp.h | 101 +++++++ .../ccp_call_control_server/CMakeLists.txt | 11 + .../ccp_call_control_server/Kconfig.sysbuild | 15 + .../ccp_call_control_server/README.rst | 77 +++++ .../nrf5340_audio_dk_nrf5340_cpuapp.conf | 6 + .../boards/nrf5340dk_nrf5340_cpuapp.conf | 6 + .../overlay-bt_ll_sw_split.conf | 5 + .../ccp_call_control_server/prj.conf | 18 ++ .../ccp_call_control_server/sample.yaml | 30 ++ .../ccp_call_control_server/src/main.c | 256 +++++++++++++++++ .../ccp_call_control_server/sysbuild.cmake | 24 ++ samples/bluetooth/tmap_central/CMakeLists.txt | 2 +- samples/bluetooth/tmap_central/prj.conf | 1 + ...ccp_server.c => ccp_call_control_server.c} | 2 +- samples/bluetooth/tmap_central/src/main.c | 4 +- .../bluetooth/tmap_central/src/tmap_central.h | 4 +- samples/bluetooth/tmap_peripheral/prj.conf | 2 +- subsys/bluetooth/audio/CMakeLists.txt | 1 + subsys/bluetooth/audio/Kconfig | 1 + subsys/bluetooth/audio/Kconfig.ccp | 34 +++ .../bluetooth/audio/ccp_call_control_server.c | 106 +++++++ subsys/bluetooth/audio/shell/CMakeLists.txt | 4 + .../audio/shell/ccp_call_control_server.c | 100 +++++++ .../ccp_call_control_server/CMakeLists.txt | 17 ++ .../audio/ccp_call_control_server/prj.conf | 21 ++ .../audio/ccp_call_control_server/src/main.c | 264 ++++++++++++++++++ .../ccp_call_control_server/testcase.yaml | 7 + .../uut/CMakeLists.txt | 22 ++ tests/bluetooth/shell/audio.conf | 4 +- tests/bluetooth/shell/testcase.yaml | 7 + tests/bluetooth/tester/overlay-le-audio.conf | 1 + tests/bsim/bluetooth/audio/prj.conf | 3 +- .../audio/src/ccp_call_control_server_test.c | 171 ++++++++++++ tests/bsim/bluetooth/audio/src/main.c | 2 + .../bsim/bluetooth/audio/test_scripts/ccp.sh | 22 ++ .../ccp/call_control_server/CMakeLists.txt | 23 ++ .../ccp/call_control_server/Kconfig.sysbuild | 10 + .../ccp/call_control_server/prj.conf | 2 + .../ccp/call_control_server/src/test_main.c | 67 +++++ .../ccp/call_control_server/sysbuild.cmake | 6 + .../bluetooth/audio_samples/ccp/compile.sh | 26 ++ .../audio_samples/ccp/tests_scripts/ccp.sh | 20 ++ tests/bsim/bluetooth/audio_samples/compile.sh | 1 + 48 files changed, 1758 insertions(+), 230 deletions(-) create mode 100644 doc/connectivity/bluetooth/shell/audio/tbs.rst create mode 100644 include/zephyr/bluetooth/audio/ccp.h create mode 100644 samples/bluetooth/ccp_call_control_server/CMakeLists.txt create mode 100644 samples/bluetooth/ccp_call_control_server/Kconfig.sysbuild create mode 100644 samples/bluetooth/ccp_call_control_server/README.rst create mode 100644 samples/bluetooth/ccp_call_control_server/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf create mode 100644 samples/bluetooth/ccp_call_control_server/boards/nrf5340dk_nrf5340_cpuapp.conf create mode 100644 samples/bluetooth/ccp_call_control_server/overlay-bt_ll_sw_split.conf create mode 100644 samples/bluetooth/ccp_call_control_server/prj.conf create mode 100644 samples/bluetooth/ccp_call_control_server/sample.yaml create mode 100644 samples/bluetooth/ccp_call_control_server/src/main.c create mode 100644 samples/bluetooth/ccp_call_control_server/sysbuild.cmake rename samples/bluetooth/tmap_central/src/{ccp_server.c => ccp_call_control_server.c} (96%) create mode 100644 subsys/bluetooth/audio/Kconfig.ccp create mode 100644 subsys/bluetooth/audio/ccp_call_control_server.c create mode 100644 subsys/bluetooth/audio/shell/ccp_call_control_server.c create mode 100644 tests/bluetooth/audio/ccp_call_control_server/CMakeLists.txt create mode 100644 tests/bluetooth/audio/ccp_call_control_server/prj.conf create mode 100644 tests/bluetooth/audio/ccp_call_control_server/src/main.c create mode 100644 tests/bluetooth/audio/ccp_call_control_server/testcase.yaml create mode 100644 tests/bluetooth/audio/ccp_call_control_server/uut/CMakeLists.txt create mode 100644 tests/bsim/bluetooth/audio/src/ccp_call_control_server_test.c create mode 100755 tests/bsim/bluetooth/audio/test_scripts/ccp.sh create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_server/CMakeLists.txt create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_server/Kconfig.sysbuild create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_server/prj.conf create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_server/src/test_main.c create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_server/sysbuild.cmake create mode 100755 tests/bsim/bluetooth/audio_samples/ccp/compile.sh create mode 100755 tests/bsim/bluetooth/audio_samples/ccp/tests_scripts/ccp.sh diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index 051824bc4a4575..de401452012c63 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -397,6 +397,7 @@ Bluetooth Host: - doc/connectivity/bluetooth/shell/audio/ - samples/bluetooth/bap*/ - samples/bluetooth/cap*/ + - samples/bluetooth/ccp*/ - samples/bluetooth/hap*/ - samples/bluetooth/hci_*/ - samples/bluetooth/pbp*/ @@ -481,6 +482,7 @@ Bluetooth Audio: - doc/connectivity/bluetooth/shell/audio/ - samples/bluetooth/bap*/ - samples/bluetooth/cap*/ + - samples/bluetooth/ccp*/ - samples/bluetooth/hap*/ - samples/bluetooth/pbp*/ - samples/bluetooth/tmap*/ diff --git a/doc/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.rst b/doc/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.rst index 1156f2a4e89e9a..7eac2171a3280e 100644 --- a/doc/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.rst +++ b/doc/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.rst @@ -294,6 +294,7 @@ GAF and the top layer profiles gave been implemented in Zephyr with the followin cluster=true; label="CCP"; style=solid; + CCP_H [label="ccp.h"]; TBS_H [label="tbs.h"]; } } @@ -331,6 +332,7 @@ GAF and the top layer profiles gave been implemented in Zephyr with the followin CAP_H -> MCS_H; CAP_H -> MCC_H; CAP_H -> MP_H; + CAP_H -> CCP_H; CAP_H -> TBS_H; CAP_H -> BAP_H; CAP_H -> BAP_PRESET_H; @@ -341,6 +343,7 @@ GAF and the top layer profiles gave been implemented in Zephyr with the followin CSIP_H -> MCS_H; CSIP_H -> MCC_H; CSIP_H -> MP_H; + CSIP_H -> CCP_H; CSIP_H -> TBS_H; CSIP_H -> BAP_H; CSIP_H -> BAP_PRESET_H; @@ -719,8 +722,8 @@ Bluetooth Audio Stack. | | | | | - Shell Module | | | | | | | - BSIM test | | +--------+-------------------------------+---------+------------------+-----------------------+--------------------------------------------------+ - | CCP | Call Control Server | 1.0 | 3.0 | - Feature complete | - API refactor | - | | | | | - Shell Module | - Sample Application | + | CCP | Call Control Server | 1.0 | 3.0 | - Feature complete | - API refactor (in progress) | + | | | | | - Shell Module | - Sample Application (in progress) | | | | | | - BSIM test | | | +-------------------------------+---------+------------------+-----------------------+--------------------------------------------------+ | | Call Control Client | 1.0 | 3.0 | - Feature complete | - API refactor | diff --git a/doc/connectivity/bluetooth/bluetooth-shell.rst b/doc/connectivity/bluetooth/bluetooth-shell.rst index 1cfa5d80546893..95fda5c2f61d90 100644 --- a/doc/connectivity/bluetooth/bluetooth-shell.rst +++ b/doc/connectivity/bluetooth/bluetooth-shell.rst @@ -19,6 +19,7 @@ For specific Bluetooth functionality see also the following shell documentation shell/audio/csip.rst shell/audio/gmap.rst shell/audio/mcp.rst + shell/audio/tbs.rst shell/audio/tmap.rst shell/audio/pbp.rst shell/classic/a2dp.rst diff --git a/doc/connectivity/bluetooth/shell/audio/ccp.rst b/doc/connectivity/bluetooth/shell/audio/ccp.rst index 9d5c17c4e7c9c2..1dc256a78befec 100644 --- a/doc/connectivity/bluetooth/shell/audio/ccp.rst +++ b/doc/connectivity/bluetooth/shell/audio/ccp.rst @@ -1,208 +1,24 @@ Bluetooth: Call Control Profile Shell ##################################### -This document describes how to run the call control functionality, both as -a client and as a (telephone bearer service (TBS)) server. Note that in the -examples below, some lines of debug have been removed to make this shorter -and provide a better overview. +Call Control Server +******************* +The Call Control Server is a role that typically resides on devices that can make calls, +including calls from apps such as Skype, e.g. (smart)phones and PCs, +which are typically GAP Central devices. -Telephone Bearer Service Client -******************************* - -The telephone bearer service client will typically exist on a resource -restricted device, such as headphones, but may also exist on e.g. phones or -laptops. The call control client will also thus typically be the advertiser. -The client can control the states of calls on a server using the call control -point. - -It is necessary to have :kconfig:option:`CONFIG_BT_TBS_CLIENT_LOG_LEVEL_DBG` -enabled for using the client interactively. - -Using the telephone bearer service client -========================================= - -When the Bluetooth stack has been initialized (:code:`bt init`), -and a device has been connected, the telephone bearer service client can -discover TBS on the connected device calling :code:`tbs_client discover`, which -will start a discovery for the TBS UUIDs and store the handles, and optionally -subscribe to all notifications (default is to subscribe to all). - -Since a server may have multiple TBS instances, most of the tbs_client commands -will take an index (starting from 0) as input. Joining calls require at least 2 -call IDs, and all call indexes shall be on the same TBS instance. - -A server will also have a GTBS instance, which is an abstraction layer for all -the telephone bearers on the server. If the server has both GTBS and TBS, -the client may subscribe and use either when sending requests if -:code:`BT_TBS_CLIENT_GTBS` is enabled. - -.. code-block:: console - - tbs_client --help - tbs_client - Bluetooth TBS_CLIENT shell commands - Subcommands: - discover :Discover TBS [subscribe] - set_signal_reporting_interval :Set the signal reporting interval - [<{instance_index, gtbs}>] - originate :Originate a call [<{instance_index, gtbs}>] - - terminate :terminate a call [<{instance_index, gtbs}>] - - accept :Accept a call [<{instance_index, gtbs}>] - hold :Place a call on hold [<{instance_index, - gtbs}>] - retrieve :Retrieve a held call [<{instance_index, - gtbs}>] - read_provider_name :Read the bearer name [<{instance_index, - gtbs}>] - read_bearer_uci :Read the bearer UCI [<{instance_index, gtbs}>] - read_technology :Read the bearer technology [<{instance_index, - gtbs}>] - read_uri_list :Read the bearer's supported URI list - [<{instance_index, gtbs}>] - read_signal_strength :Read the bearer signal strength - [<{instance_index, gtbs}>] - read_signal_interval :Read the bearer signal strength reporting - interval [<{instance_index, gtbs}>] - read_current_calls :Read the current calls [<{instance_index, - gtbs}>] - read_ccid :Read the CCID [<{instance_index, gtbs}>] - read_status_flags :Read the in feature and status value - [<{instance_index, gtbs}>] - read_uri :Read the incoming call target URI - [<{instance_index, gtbs}>] - read_call_state :Read the call state [<{instance_index, gtbs}>] - read_remote_uri :Read the incoming remote URI - [<{instance_index, gtbs}>] - read_friendly_name :Read the friendly name of an incoming call - [<{instance_index, gtbs}>] - read_optional_opcodes :Read the optional opcodes [<{instance_index, - gtbs}>] - - -In the following examples, notifications from GTBS is ignored, unless otherwise -specified. - -Example usage -============= - -Setup ------ - -.. code-block:: console - - uart:~$ bt init - uart:~$ bt advertise on - Advertising started - -When connected --------------- - -Placing a call: - -.. code-block:: console - - uart:~$ tbs_client discover - bt_tbs_client.primary_discover_func: Discover complete, found 1 instances (GTBS found) - bt_tbs_client.discover_func: Setup complete for 1 / 1 TBS - bt_tbs_client.discover_func: Setup complete GTBS - uart:~$ tbs_client originate 0 tel:123 - bt_tbs_client.notify_handler: Index 0 - bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the dialing state with URI tel:123 - bt_tbs_client.call_cp_notify_handler: Status: success for the originate opcode for call 0x00 - bt_tbs_client.notify_handler: Index 0 - bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the alerting state with URI tel:123 - - bt_tbs_client.notify_handler: Index 0 - bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the active state with URI tel:123 - -Placing a call on GTBS: - -.. code-block:: console - - uart:~$ tbs_client originate 0 tel:123 - bt_tbs_client.notify_handler: Index 0 - bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the dialing state with URI tel:123 - bt_tbs_client.call_cp_notify_handler: Status: success for the originate opcode for call 0x00 - bt_tbs_client.notify_handler: Index 0 - bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the alerting state with URI tel:123 - - bt_tbs_client.notify_handler: Index 0 - bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the active state with URI tel:123 - -It is necessary to set an outgoing caller ID before placing a call. - -Accepting incoming call from peer device: - -.. code-block:: console - - bt_tbs_client.incoming_uri_notify_handler: tel:123 - bt_tbs_client.in_call_notify_handler: tel:456 - bt_tbs_client.friendly_name_notify_handler: Peter - bt_tbs_client.current_calls_notify_handler: Call 0x05 is in the incoming state with URI tel:456 - uart:~$ tbs_client accept 0 5 - bt_tbs_client.call_cp_callback_handler: Status: success for the accept opcode for call 0x05 - bt_tbs_client.current_calls_notify_handler: Call 0x05 is in the active state with URI tel - - -Terminate call: - -.. code-block:: console - - uart:~$ tbs_client terminate 0 5 - bt_tbs_client.termination_reason_notify_handler: ID 0x05, reason 0x06 - bt_tbs_client.call_cp_notify_handler: Status: success for the terminate opcode for call 0x05 - bt_tbs_client.current_calls_notify_handler: - -Telephone Bearer Service (TBS) -****************************** -The telephone bearer service is a service that typically resides on devices that -can make calls, including calls from apps such as Skype, e.g. (smart)phones and -PCs. - -It is necessary to have :kconfig:option:`CONFIG_BT_TBS_LOG_LEVEL_DBG` enabled -for using the TBS server interactively. - -Using the telephone bearer service -================================== -TBS can be controlled locally, or by a remote device (when in a call). For -example a remote device may initiate a call to the device with the TBS server, -or the TBS server may initiate a call to remote device, without a TBS_CLIENT client. -The TBS implementation is capable of fully controlling any call. -Omitting an index for commands where a :code:`` can be supplied, defaults to the -GTBS bearer. +Using the Call Control Server +============================= +The Server can be controlled locally, or by a remote device (when in a call). For +example a remote device may initiate a call to the server, +or the Server may initiate a call to remote device, without a client. .. code-block:: console - tbs --help - tbs - Bluetooth TBS shell commands + ccp_call_control_server --help + ccp_call_control_server - Bluetooth CCP Call Control Server shell commands Subcommands: - init :Initialize TBS - authorize :Authorize the current connection - accept :Accept call - terminate :Terminate call - hold :Hold call - retrieve :Retrieve call - originate :Originate call [] - join :Join calls [ [ [...]]] - incoming :Simulate incoming remote call [<{instance_index, - gtbs}>] - - remote_answer :Simulate remote answer outgoing call - remote_retrieve :Simulate remote retrieve - remote_terminate :Simulate remote terminate - remote_hold :Simulate remote hold - set_bearer_provider_name :Set the bearer provider name [<{instance_index, - gtbs}>] - set_bearer_technology :Set the bearer technology [<{instance_index, - gtbs}>] - set_bearer_signal_strength :Set the bearer signal strength [<{instance_index, - gtbs}>] - set_status_flags :Set the bearer feature and status value - [<{instance_index, gtbs}>] - set_uri_scheme :Set the URI prefix list - print_calls :Output all calls in the debug log + init : Initialize CCP Call Control Server Example Usage ============= @@ -213,26 +29,7 @@ Setup .. code-block:: console uart:~$ bt init + uart:~$ ccp_call_control_server init + Registered GTBS bearer + Registered bearer[1] uart:~$ bt connect xx:xx:xx:xx:xx:xx public - -When connected --------------- - -Answering a call for a peer device originated by a client: - -.. code-block:: console - - bt_tbs.write_call_cp: Index 0: Processing the originate opcode - bt_tbs.originate_call: New call with call index 1 - bt_tbs.write_call_cp: Index 0: Processed the originate opcode with status success for call index 1 - uart:~$ tbs remote_answer 1 - TBS succeeded for call_id: 1 - -Incoming call from a peer device, accepted by client: - -.. code-block:: console - - uart:~$ tbs incoming 0 tel:123 tel:456 Peter - TBS succeeded for call_id: 4 - bt_tbs.bt_tbs_remote_incoming: New call with call index 4 - bt_tbs.write_call_cp: Index 0: Processed the accept opcode with status success for call index 4 diff --git a/doc/connectivity/bluetooth/shell/audio/tbs.rst b/doc/connectivity/bluetooth/shell/audio/tbs.rst new file mode 100644 index 00000000000000..e4eb3efebf346d --- /dev/null +++ b/doc/connectivity/bluetooth/shell/audio/tbs.rst @@ -0,0 +1,237 @@ +Bluetooth: Telephone Bearer Service Shell +######################################### + +This document describes how to run the call control functionality, both as +a client and as a (telephone bearer service (TBS)) server. Note that in the +examples below, some lines of debug have been removed to make this shorter +and provide a better overview. + +Telephone Bearer Service Client +******************************* + +The telephone bearer service client will typically exist on a resource +restricted device, such as headphones, but may also exist on e.g. phones or +laptops. The call control client will also thus typically be the advertiser. +The client can control the states of calls on a server using the call control +point. + +It is necessary to have :kconfig:option:`CONFIG_BT_TBS_CLIENT_LOG_LEVEL_DBG` +enabled for using the client interactively. + +Using the telephone bearer service client +========================================= + +When the Bluetooth stack has been initialized (:code:`bt init`), +and a device has been connected, the telephone bearer service client can +discover TBS on the connected device calling :code:`tbs_client discover`, which +will start a discovery for the TBS UUIDs and store the handles, and optionally +subscribe to all notifications (default is to subscribe to all). + +Since a server may have multiple TBS instances, most of the tbs_client commands +will take an index (starting from 0) as input. Joining calls require at least 2 +call IDs, and all call indexes shall be on the same TBS instance. + +A server will also have a GTBS instance, which is an abstraction layer for all +the telephone bearers on the server. If the server has both GTBS and TBS, +the client may subscribe and use either when sending requests if +:code:`BT_TBS_CLIENT_GTBS` is enabled. + +.. code-block:: console + + tbs_client --help + tbs_client - Bluetooth TBS_CLIENT shell commands + Subcommands: + discover :Discover TBS [subscribe] + set_signal_reporting_interval :Set the signal reporting interval + [<{instance_index, gtbs}>] + originate :Originate a call [<{instance_index, gtbs}>] + + terminate :terminate a call [<{instance_index, gtbs}>] + + accept :Accept a call [<{instance_index, gtbs}>] + hold :Place a call on hold [<{instance_index, + gtbs}>] + retrieve :Retrieve a held call [<{instance_index, + gtbs}>] + read_provider_name :Read the bearer name [<{instance_index, + gtbs}>] + read_bearer_uci :Read the bearer UCI [<{instance_index, gtbs}>] + read_technology :Read the bearer technology [<{instance_index, + gtbs}>] + read_uri_list :Read the bearer's supported URI list + [<{instance_index, gtbs}>] + read_signal_strength :Read the bearer signal strength + [<{instance_index, gtbs}>] + read_signal_interval :Read the bearer signal strength reporting + interval [<{instance_index, gtbs}>] + read_current_calls :Read the current calls [<{instance_index, + gtbs}>] + read_ccid :Read the CCID [<{instance_index, gtbs}>] + read_status_flags :Read the in feature and status value + [<{instance_index, gtbs}>] + read_uri :Read the incoming call target URI + [<{instance_index, gtbs}>] + read_call_state :Read the call state [<{instance_index, gtbs}>] + read_remote_uri :Read the incoming remote URI + [<{instance_index, gtbs}>] + read_friendly_name :Read the friendly name of an incoming call + [<{instance_index, gtbs}>] + read_optional_opcodes :Read the optional opcodes [<{instance_index, + gtbs}>] + + +In the following examples, notifications from GTBS are ignored, unless otherwise specified. + +Example usage +============= + +Setup +----- + +.. code-block:: console + + uart:~$ bt init + uart:~$ bt advertise on + Advertising started + +When connected +-------------- + +Placing a call: + +.. code-block:: console + + uart:~$ tbs_client discover + bt_tbs_client.primary_discover_func: Discover complete, found 1 instances (GTBS found) + bt_tbs_client.discover_func: Setup complete for 1 / 1 TBS + bt_tbs_client.discover_func: Setup complete GTBS + uart:~$ tbs_client originate 0 tel:123 + bt_tbs_client.notify_handler: Index 0 + bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the dialing state with URI tel:123 + bt_tbs_client.call_cp_notify_handler: Status: success for the originate opcode for call 0x00 + bt_tbs_client.notify_handler: Index 0 + bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the alerting state with URI tel:123 + + bt_tbs_client.notify_handler: Index 0 + bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the active state with URI tel:123 + +Placing a call on GTBS: + +.. code-block:: console + + uart:~$ tbs_client originate 0 tel:123 + bt_tbs_client.notify_handler: Index 0 + bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the dialing state with URI tel:123 + bt_tbs_client.call_cp_notify_handler: Status: success for the originate opcode for call 0x00 + bt_tbs_client.notify_handler: Index 0 + bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the alerting state with URI tel:123 + + bt_tbs_client.notify_handler: Index 0 + bt_tbs_client.current_calls_notify_handler: Call 0x01 is in the active state with URI tel:123 + +It is necessary to set an outgoing caller ID before placing a call. + +Accepting incoming call from peer device: + +.. code-block:: console + + bt_tbs_client.incoming_uri_notify_handler: tel:123 + bt_tbs_client.in_call_notify_handler: tel:456 + bt_tbs_client.friendly_name_notify_handler: Peter + bt_tbs_client.current_calls_notify_handler: Call 0x05 is in the incoming state with URI tel:456 + uart:~$ tbs_client accept 0 5 + bt_tbs_client.call_cp_callback_handler: Status: success for the accept opcode for call 0x05 + bt_tbs_client.current_calls_notify_handler: Call 0x05 is in the active state with URI tel + + +Terminate call: + +.. code-block:: console + + uart:~$ tbs_client terminate 0 5 + bt_tbs_client.termination_reason_notify_handler: ID 0x05, reason 0x06 + bt_tbs_client.call_cp_notify_handler: Status: success for the terminate opcode for call 0x05 + bt_tbs_client.current_calls_notify_handler: + +Telephone Bearer Service (TBS) +****************************** +The telephone bearer service is a service that typically resides on devices that +can make calls, including calls from apps such as Skype, e.g. (smart)phones and +PCs. + +It is necessary to have :kconfig:option:`CONFIG_BT_TBS_LOG_LEVEL_DBG` enabled +for using the TBS server interactively. + +Using the telephone bearer service +================================== +TBS can be controlled locally, or by a remote device (when in a call). For +example a remote device may initiate a call to the device with the TBS server, +or the TBS server may initiate a call to remote device, without a TBS_CLIENT client. +The TBS implementation is capable of fully controlling any call. +Omitting an index for commands where a :code:`` can be supplied, defaults to the +GTBS bearer. + +.. code-block:: console + + tbs --help + tbs - Bluetooth TBS shell commands + Subcommands: + init :Initialize TBS + authorize :Authorize the current connection + accept :Accept call + terminate :Terminate call + hold :Hold call + retrieve :Retrieve call + originate :Originate call [] + join :Join calls [ [ [...]]] + incoming :Simulate incoming remote call [<{instance_index, + gtbs}>] + + remote_answer :Simulate remote answer outgoing call + remote_retrieve :Simulate remote retrieve + remote_terminate :Simulate remote terminate + remote_hold :Simulate remote hold + set_bearer_provider_name :Set the bearer provider name [<{instance_index, + gtbs}>] + set_bearer_technology :Set the bearer technology [<{instance_index, + gtbs}>] + set_bearer_signal_strength :Set the bearer signal strength [<{instance_index, + gtbs}>] + set_status_flags :Set the bearer feature and status value + [<{instance_index, gtbs}>] + set_uri_scheme :Set the URI prefix list + print_calls :Output all calls in the debug log + +Example Usage +============= + +Setup +----- + +.. code-block:: console + + uart:~$ bt init + uart:~$ bt connect xx:xx:xx:xx:xx:xx public + +When connected +-------------- + +Answering a call for a peer device originated by a client: + +.. code-block:: console + + bt_tbs.write_call_cp: Index 0: Processing the originate opcode + bt_tbs.originate_call: New call with call index 1 + bt_tbs.write_call_cp: Index 0: Processed the originate opcode with status success for call index 1 + uart:~$ tbs remote_answer 1 + TBS succeeded for call_id: 1 + +Incoming call from a peer device, accepted by client: + +.. code-block:: console + + uart:~$ tbs incoming 0 tel:123 tel:456 Peter + TBS succeeded for call_id: 4 + bt_tbs.bt_tbs_remote_incoming: New call with call index 4 + bt_tbs.write_call_cp: Index 0: Processed the accept opcode with status success for call index 4 diff --git a/include/zephyr/bluetooth/audio/ccp.h b/include/zephyr/bluetooth/audio/ccp.h new file mode 100644 index 00000000000000..4bd8b53fd8c72e --- /dev/null +++ b/include/zephyr/bluetooth/audio/ccp.h @@ -0,0 +1,101 @@ +/** + * @file + * @brief Bluetooth Call Control Profile (CCP) APIs. + */ + +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_CCP_H_ +#define ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_CCP_H_ + +/** + * @brief Call Control Profile (CCP) + * + * @defgroup bt_ccp Call Control Profile (CCP) + * + * @since 3.7 + * @version 0.1.0 + * + * @ingroup bluetooth + * @{ + * + * Call Control Profile (CCP) provides procedures to initiate and control calls. + * It provides the Call Control Client and the Call Control Server roles, + * where the former is usually placed on resource constrained devices like headphones, + * and the latter placed on more powerful devices like phones and PCs. + * + * The profile is not limited to carrier phone calls and can be used with common applications like + * Discord and Teams. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif +/** + * @defgroup bt_ccp_call_control_server CCP Call Control Server APIs + * @ingroup bt_ccp + * @{ + */ +/** @brief Abstract Call Control Server Telephone Bearer structure. */ +struct bt_ccp_call_control_server_bearer; + +/** + * @brief Register a Telephone Bearer + * + * This will register a Telephone Bearer Service (TBS) (or a Generic Telephone Bearer service + * (GTBS)) with the provided parameters. + * + * As per the TBS specification, the GTBS shall be instantiated for the feature, + * and as such a GTBS shall always be registered before any TBS can be registered. + * Similarly, all TBS shall be unregistered before the GTBS can be unregistered with + * bt_ccp_call_control_server_unregister_bearer(). + * + * @param[in] param The parameters to initialize the bearer. + * @param[out] bearer Pointer to the initialized bearer. + * + * @retval 0 Success + * @retval -EINVAL @p param contains invalid data + * @retval -EALREADY @p param.gtbs is true and GTBS has already been registered + * @retval -EAGAIN @p param.gtbs is false and GTBS has not been registered + * @retval -ENOMEM @p param.gtbs is false and no more TBS can be registered (see + * @kconfig{CONFIG_BT_TBS_BEARER_COUNT}) + * @retval -ENOEXEC The service failed to be registered + */ +int bt_ccp_call_control_server_register_bearer(const struct bt_tbs_register_param *param, + struct bt_ccp_call_control_server_bearer **bearer); + +/** + * @brief Unregister a Telephone Bearer + * + * This will unregister a Telephone Bearer Service (TBS) (or a Generic Telephone Bearer service + * (GTBS)) with the provided parameters. The bearer shall be registered first by + * bt_ccp_call_control_server_register_bearer() before it can be unregistered. + * + * All TBS shall be unregistered before the GTBS can be unregistered with. + * + * @param bearer The bearer to unregister. + * + * @retval 0 Success + * @retval -EINVAL @p bearer is NULL + * @retval -EALREADY The bearer is not registered + * @retval -ENOEXEC The service failed to be unregistered + */ +int bt_ccp_call_control_server_unregister_bearer(struct bt_ccp_call_control_server_bearer *bearer); + +/** @} */ /* End of group bt_ccp_call_control_server */ + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_BLUETOOTH_AUDIO_CCP_H_ */ diff --git a/samples/bluetooth/ccp_call_control_server/CMakeLists.txt b/samples/bluetooth/ccp_call_control_server/CMakeLists.txt new file mode 100644 index 00000000000000..67e198d981480e --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/CMakeLists.txt @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ccp_call_control_server) + +target_sources(app PRIVATE + src/main.c +) + +zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) diff --git a/samples/bluetooth/ccp_call_control_server/Kconfig.sysbuild b/samples/bluetooth/ccp_call_control_server/Kconfig.sysbuild new file mode 100644 index 00000000000000..f37b265ecbc27b --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/Kconfig.sysbuild @@ -0,0 +1,15 @@ +# Copyright 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "share/sysbuild/Kconfig" + +config NET_CORE_BOARD + string + default "nrf5340dk/nrf5340/cpunet" if "$(BOARD)" = "nrf5340dk" + default "nrf5340_audio_dk/nrf5340/cpunet" if "$(BOARD)" = "nrf5340_audio_dk" + default "nrf5340bsim/nrf5340/cpunet" if $(BOARD_TARGET_STRING) = "NRF5340BSIM_NRF5340_CPUAPP" + +config NET_CORE_IMAGE_HCI_IPC + bool "HCI IPC image on network core" + default y + depends on NET_CORE_BOARD != "" diff --git a/samples/bluetooth/ccp_call_control_server/README.rst b/samples/bluetooth/ccp_call_control_server/README.rst new file mode 100644 index 00000000000000..eab3bbbdc0ab92 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/README.rst @@ -0,0 +1,77 @@ +.. zephyr:code-sample:: bluetooth_ccp_call_control_server + :name: Call Control Profile (CCP) Call Control Server + :relevant-api: bluetooth bt_ccp bt_tbs + + CCP Call Control Server sample that registers one or more TBS bearers and advertises the + TBS UUID(s). + +Overview +******** + +Application demonstrating the CCP Call Control Server functionality. +Starts by advertising for CCP Call Control Clients to connect and set up calls. + +The profile works for both GAP Central and GAP Peripheral devices, but this sample only assumes the +GAP Peripheral role. + +This sample can be found under :zephyr_file:`samples/bluetooth/ccp_call_control_server` in the Zephyr tree. + +Check the :zephyr:code-sample-category:`bluetooth` samples for general information. + +Requirements +************ + +* BlueZ running on the host, or +* A board with Bluetooth Low Energy 5.2 support + +Building and Running +******************** + +When building targeting an nrf52 series board with the Zephyr Bluetooth Controller, +use ``-DOVERLAY_CONFIG=overlay-bt_ll_sw_split.conf`` to enable the required feature support. + +Building for an nrf5340dk +------------------------- + +You can build both the application core image and an appropriate controller image for the network +core with: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/ccp_call_control_server/ + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +If you prefer to only build the application core image, you can do so by doing instead: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/ccp_call_control_server/ + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + +In that case you can pair this application core image with the +:zephyr:code-sample:`bluetooth_hci_ipc` sample +:zephyr_file:`samples/bluetooth/hci_ipc/nrf5340_cpunet_iso-bt_ll_sw_split.conf` configuration. + +Building for a simulated nrf5340bsim +------------------------------------ + +Similarly to how you would for real HW, you can do: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/ccp_call_control_server/ + :board: nrf5340bsim/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +Note this will produce a Linux executable in :file:`./build/zephyr/zephyr.exe`. +For more information, check :ref:`this board documentation `. + +Building for a simulated nrf52_bsim +----------------------------------- + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/ccp_call_control_server/ + :board: nrf52_bsim + :goals: build + :gen-args: -DOVERLAY_CONFIG=overlay-bt_ll_sw_split.conf diff --git a/samples/bluetooth/ccp_call_control_server/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf b/samples/bluetooth/ccp_call_control_server/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf new file mode 100644 index 00000000000000..f58eedb0453c57 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf @@ -0,0 +1,6 @@ +CONFIG_BT_BUF_EVT_RX_SIZE=255 +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 + +CONFIG_BT_SEND_ECC_EMULATION=y diff --git a/samples/bluetooth/ccp_call_control_server/boards/nrf5340dk_nrf5340_cpuapp.conf b/samples/bluetooth/ccp_call_control_server/boards/nrf5340dk_nrf5340_cpuapp.conf new file mode 100644 index 00000000000000..f58eedb0453c57 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/boards/nrf5340dk_nrf5340_cpuapp.conf @@ -0,0 +1,6 @@ +CONFIG_BT_BUF_EVT_RX_SIZE=255 +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 + +CONFIG_BT_SEND_ECC_EMULATION=y diff --git a/samples/bluetooth/ccp_call_control_server/overlay-bt_ll_sw_split.conf b/samples/bluetooth/ccp_call_control_server/overlay-bt_ll_sw_split.conf new file mode 100644 index 00000000000000..6c1e35227169af --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/overlay-bt_ll_sw_split.conf @@ -0,0 +1,5 @@ +# Zephyr Bluetooth Controller +CONFIG_BT_LL_SW_SPLIT=y + +# Zephyr Controller tested maximum advertising data that can be set in a single HCI command +CONFIG_BT_CTLR_ADV_DATA_LEN_MAX=191 diff --git a/samples/bluetooth/ccp_call_control_server/prj.conf b/samples/bluetooth/ccp_call_control_server/prj.conf new file mode 100644 index 00000000000000..c05cd8850ab283 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/prj.conf @@ -0,0 +1,18 @@ +CONFIG_BT=y +CONFIG_LOG=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_AUDIO=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_SMP=y +CONFIG_BT_GATT_DYNAMIC_DB=y +CONFIG_BT_DEVICE_NAME="CCP Call Control Server" + +CONFIG_BT_SMP=y +CONFIG_BT_KEYS_OVERWRITE_OLDEST=y + +# CCP support +CONFIG_BT_CCP_CALL_CONTROL_SERVER=y +CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT=2 +CONFIG_BT_TBS=y +CONFIG_BT_TBS_BEARER_COUNT=1 +CONFIG_UTF8=y diff --git a/samples/bluetooth/ccp_call_control_server/sample.yaml b/samples/bluetooth/ccp_call_control_server/sample.yaml new file mode 100644 index 00000000000000..501c5b374210d5 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/sample.yaml @@ -0,0 +1,30 @@ +sample: + description: Bluetooth Low Energy Call Control Profile Call Control Server sample + name: Bluetooth Low Energy Call Control Profile Call Control Server sample +tests: + sample.bluetooth.ccp_call_control_server: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf5340dk/nrf5340/cpuapp + - nrf5340bsim/nrf5340/cpuapp + integration_platforms: + - qemu_x86 + - nrf5340dk/nrf5340/cpuapp + tags: bluetooth + sysbuild: true + sample.bluetooth.ccp_call_control_server.bt_ll_sw_split: + harness: bluetooth + platform_allow: + - nrf52_bsim + - nrf52833dk/nrf52833 + - nrf52840dk/nrf52840 + - nrf52840dongle/nrf52840 + integration_platforms: + - nrf52_bsim + - nrf52833dk/nrf52833 + - nrf52840dk/nrf52840 + - nrf52840dongle/nrf52840 + extra_args: OVERLAY_CONFIG=overlay-bt_ll_sw_split.conf + tags: bluetooth diff --git a/samples/bluetooth/ccp_call_control_server/src/main.c b/samples/bluetooth/ccp_call_control_server/src/main.c new file mode 100644 index 00000000000000..df0c37b811f376 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/src/main.c @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(ccp_call_control_server, CONFIG_LOG_DEFAULT_LEVEL); + +#define SEM_TIMEOUT K_SECONDS(5) + +static const struct bt_data ad[] = { + BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1), + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA_BYTES(BT_DATA_UUID16_SOME, BT_UUID_16_ENCODE(BT_UUID_GTBS_VAL)), + BT_DATA_BYTES(BT_DATA_SVC_DATA16, BT_UUID_16_ENCODE(BT_UUID_GTBS_VAL)), + IF_ENABLED(CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT > 1, + (BT_DATA_BYTES(BT_DATA_UUID16_SOME, BT_UUID_16_ENCODE(BT_UUID_TBS_VAL)), + BT_DATA_BYTES(BT_DATA_SVC_DATA16, BT_UUID_16_ENCODE(BT_UUID_TBS_VAL))))}; + +static struct bt_le_ext_adv *adv; +static struct bt_conn *peer_conn; +static struct bt_ccp_call_control_server_bearer + *bearers[CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT]; + +static K_SEM_DEFINE(sem_state_change, 0, 1); + +static void connected_cb(struct bt_conn *conn, uint8_t err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + LOG_INF("Connected: %s", addr); + + peer_conn = bt_conn_ref(conn); + k_sem_give(&sem_state_change); +} + +static void disconnected_cb(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + if (conn != peer_conn) { + return; + } + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + LOG_INF("Disconnected: %s (reason 0x%02x)", addr, reason); + + bt_conn_unref(peer_conn); + peer_conn = NULL; + k_sem_give(&sem_state_change); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected_cb, + .disconnected = disconnected_cb, +}; + +static int advertise(void) +{ + int err; + + err = bt_le_ext_adv_create(BT_LE_EXT_ADV_CONN, NULL, &adv); + if (err) { + LOG_ERR("Failed to create advertising set: %d", err); + + return err; + } + + err = bt_le_ext_adv_set_data(adv, ad, ARRAY_SIZE(ad), NULL, 0); + if (err) { + LOG_ERR("Failed to set advertising data: %d", err); + + return err; + } + + err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT); + if (err) { + LOG_ERR("Failed to start advertising set: %d", err); + + return err; + } + + LOG_INF("Advertising successfully started"); + + /* Wait for connection*/ + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_state_change: err %d", err); + + return err; + } + + return 0; +} + +static int reset_ccp_call_control_server(void) +{ + int err; + + LOG_INF("Resetting"); + + if (peer_conn != NULL) { + err = bt_conn_disconnect(peer_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err != 0) { + return err; + } + + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_state_change: %d", err); + return err; + } + } + + if (adv != NULL) { + err = bt_le_ext_adv_stop(adv); + if (err != 0) { + LOG_ERR("Failed to stop advertiser: %d", err); + return err; + } + + err = bt_le_ext_adv_delete(adv); + if (err != 0) { + LOG_ERR("Failed to delete advertiser: %d", err); + return err; + } + + adv = NULL; + } + + k_sem_reset(&sem_state_change); + + return 0; +} + +static int init_ccp_call_control_server(void) +{ + const struct bt_tbs_register_param gtbs_param = { + .provider_name = "Generic TBS", + .uci = "un000", + .uri_schemes_supported = "tel,skype", + .gtbs = true, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = CONFIG_BT_TBS_SUPPORTED_FEATURES, + }; + int err; + + err = bt_enable(NULL); + if (err != 0) { + LOG_ERR("Bluetooth enable failed (err %d)", err); + + return err; + } + + LOG_DBG("Bluetooth initialized"); + + err = bt_ccp_call_control_server_register_bearer(>bs_param, &bearers[0]); + if (err < 0) { + LOG_ERR("Failed to register GTBS (err %d)", err); + + return err; + } + + LOG_INF("Registered GTBS bearer"); + + for (int i = 1; i < CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT; i++) { + char prov_name[22]; /* Enough to store "Telephone Bearer #255" */ + const struct bt_tbs_register_param tbs_param = { + .provider_name = prov_name, + .uci = "un000", + .uri_schemes_supported = "tel,skype", + .gtbs = false, + .authorization_required = false, + /* Set different technologies per bearer */ + .technology = (i % BT_TBS_TECHNOLOGY_WCDMA) + 1, + .supported_features = CONFIG_BT_TBS_SUPPORTED_FEATURES, + }; + + snprintf(prov_name, sizeof(prov_name), "Telephone Bearer #%d", i); + + err = bt_ccp_call_control_server_register_bearer(&tbs_param, &bearers[i]); + if (err < 0) { + LOG_ERR("Failed to register bearer[%d]: %d", i, err); + + return err; + } + + LOG_INF("Registered bearer[%d]", i); + } + + return 0; +} + +int main(void) +{ + int err; + + err = init_ccp_call_control_server(); + if (err != 0) { + return 0; + } + + LOG_INF("CCP Call Control Server initialized"); + + while (true) { + err = reset_ccp_call_control_server(); + if (err != 0) { + LOG_ERR("Failed to reset"); + + break; + } + + /* Start advertising as a CCP Call Control Server, which includes setting the + * required advertising data based on the roles we support. + */ + err = advertise(); + if (err != 0) { + continue; + } + + /* After advertising we expect CCP Call Control Clients to connect to us and + * eventually disconnect again. As a CCP Call Control Server we just react to their + * requests and not do anything else. + */ + + /* Reset if disconnected */ + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_state_change: err %d", err); + + break; + } + } + + return 0; +} diff --git a/samples/bluetooth/ccp_call_control_server/sysbuild.cmake b/samples/bluetooth/ccp_call_control_server/sysbuild.cmake new file mode 100644 index 00000000000000..2523aac8ea76f1 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_server/sysbuild.cmake @@ -0,0 +1,24 @@ +# Copyright (c) 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +if(SB_CONFIG_NET_CORE_IMAGE_HCI_IPC) + # For builds in the nrf5340, we build the netcore image with the controller + + set(NET_APP hci_ipc) + set(NET_APP_SRC_DIR ${ZEPHYR_BASE}/samples/bluetooth/${NET_APP}) + + ExternalZephyrProject_Add( + APPLICATION ${NET_APP} + SOURCE_DIR ${NET_APP_SRC_DIR} + BOARD ${SB_CONFIG_NET_CORE_BOARD} + ) + + set(${NET_APP}_CONF_FILE + ${NET_APP_SRC_DIR}/nrf5340_cpunet_iso-bt_ll_sw_split.conf + CACHE INTERNAL "" + ) + + native_simulator_set_child_images(${DEFAULT_IMAGE} ${NET_APP}) +endif() + +native_simulator_set_final_executable(${DEFAULT_IMAGE}) diff --git a/samples/bluetooth/tmap_central/CMakeLists.txt b/samples/bluetooth/tmap_central/CMakeLists.txt index dd1bca04e1302a..7a122c1157c894 100644 --- a/samples/bluetooth/tmap_central/CMakeLists.txt +++ b/samples/bluetooth/tmap_central/CMakeLists.txt @@ -8,7 +8,7 @@ target_sources(app PRIVATE src/main.c src/mcp_server.c src/vcp_vol_ctlr.c - src/ccp_server.c + src/ccp_call_control_server.c src/cap_initiator.c ) diff --git a/samples/bluetooth/tmap_central/prj.conf b/samples/bluetooth/tmap_central/prj.conf index 0718245e20106c..2d2b50dc1173da 100644 --- a/samples/bluetooth/tmap_central/prj.conf +++ b/samples/bluetooth/tmap_central/prj.conf @@ -35,6 +35,7 @@ CONFIG_MCTL_LOCAL_PLAYER_CONTROL=y CONFIG_MCTL=y # CCP support +CONFIG_BT_CCP_CALL_CONTROL_SERVER=y CONFIG_BT_TBS=y CONFIG_BT_TBS_SUPPORTED_FEATURES=3 diff --git a/samples/bluetooth/tmap_central/src/ccp_server.c b/samples/bluetooth/tmap_central/src/ccp_call_control_server.c similarity index 96% rename from samples/bluetooth/tmap_central/src/ccp_server.c rename to samples/bluetooth/tmap_central/src/ccp_call_control_server.c index 7f21563cfe0d1a..9ddbcfff01a6c3 100644 --- a/samples/bluetooth/tmap_central/src/ccp_server.c +++ b/samples/bluetooth/tmap_central/src/ccp_call_control_server.c @@ -44,7 +44,7 @@ static struct bt_tbs_cb tbs_cbs = { .authorize = NULL, }; -int ccp_server_init(void) +int ccp_call_control_server_init(void) { int err; diff --git a/samples/bluetooth/tmap_central/src/main.c b/samples/bluetooth/tmap_central/src/main.c index 98edc30bf9dc7c..4c47e232bc08bd 100644 --- a/samples/bluetooth/tmap_central/src/main.c +++ b/samples/bluetooth/tmap_central/src/main.c @@ -306,8 +306,8 @@ int main(void) } printk("MCP initialized\n"); - /* Initialize CCP Server */ - err = ccp_server_init(); + /* Initialize CCP Call Control Server */ + err = ccp_call_control_server_init(); if (err != 0) { return err; } diff --git a/samples/bluetooth/tmap_central/src/tmap_central.h b/samples/bluetooth/tmap_central/src/tmap_central.h index 1b2277a3b5be60..6792c033511632 100644 --- a/samples/bluetooth/tmap_central/src/tmap_central.h +++ b/samples/bluetooth/tmap_central/src/tmap_central.h @@ -18,11 +18,11 @@ int mcp_server_init(void); /** - * @brief Initialize the CCP Server role + * @brief Initialize the CCP Call Control Server role * * @return 0 if success, errno on failure. */ -int ccp_server_init(void); +int ccp_call_control_server_init(void); /** * @brief Initialize the VCP Volume Controller role diff --git a/samples/bluetooth/tmap_peripheral/prj.conf b/samples/bluetooth/tmap_peripheral/prj.conf index ebe30c7548639b..b073c27e2fa75c 100644 --- a/samples/bluetooth/tmap_peripheral/prj.conf +++ b/samples/bluetooth/tmap_peripheral/prj.conf @@ -43,7 +43,7 @@ CONFIG_BT_PAC_SNK_LOC=y # Source PAC Location Support CONFIG_BT_PAC_SRC_LOC=y -# CCP Client Support +# CCP Call Control Client Support CONFIG_BT_TBS_CLIENT_GTBS=y CONFIG_BT_TBS_CLIENT_ORIGINATE_CALL=y CONFIG_BT_TBS_CLIENT_TERMINATE_CALL=y diff --git a/subsys/bluetooth/audio/CMakeLists.txt b/subsys/bluetooth/audio/CMakeLists.txt index ef6fc16640b229..86f507746be2db 100644 --- a/subsys/bluetooth/audio/CMakeLists.txt +++ b/subsys/bluetooth/audio/CMakeLists.txt @@ -58,6 +58,7 @@ zephyr_library_sources_ifdef(CONFIG_BT_BAP_BROADCAST_SOURCE bap_broadcast_source zephyr_library_sources_ifdef(CONFIG_BT_BAP_BROADCAST_SINK bap_broadcast_sink.c) zephyr_library_sources_ifdef(CONFIG_BT_BAP_SCAN_DELEGATOR bap_scan_delegator.c) zephyr_library_sources_ifdef(CONFIG_BT_BAP_BROADCAST_ASSISTANT bap_broadcast_assistant.c) +zephyr_library_sources_ifdef(CONFIG_BT_CCP_CALL_CONTROL_SERVER ccp_call_control_server.c) zephyr_library_sources_ifdef(CONFIG_BT_HAS has.c) zephyr_library_sources_ifdef(CONFIG_BT_HAS_CLIENT has_client.c) zephyr_library_sources_ifdef(CONFIG_BT_CAP cap_stream.c) diff --git a/subsys/bluetooth/audio/Kconfig b/subsys/bluetooth/audio/Kconfig index f56c3fe5236ad6..bc3335aaf5dff2 100644 --- a/subsys/bluetooth/audio/Kconfig +++ b/subsys/bluetooth/audio/Kconfig @@ -34,6 +34,7 @@ config BT_AUDIO_NOTIFY_RETRY_DELAY available. rsource "Kconfig.bap" +rsource "Kconfig.ccp" rsource "Kconfig.vocs" rsource "Kconfig.aics" rsource "Kconfig.vcp" diff --git a/subsys/bluetooth/audio/Kconfig.ccp b/subsys/bluetooth/audio/Kconfig.ccp new file mode 100644 index 00000000000000..f43557e37a8699 --- /dev/null +++ b/subsys/bluetooth/audio/Kconfig.ccp @@ -0,0 +1,34 @@ +# Bluetooth Audio - Call Control Profile (CCP) configuration options +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +if BT_AUDIO + +config BT_CCP_CALL_CONTROL_SERVER + bool "Call Control Profile Call Control Server Support" + depends on BT_EXT_ADV + depends on BT_TBS + depends on BT_BONDABLE + help + This option enables support for the Call Control Profile Call Control Server which uses + the Telephone Bearer Service (TBS) to hold and control calls on a device. + +if BT_CCP_CALL_CONTROL_SERVER + +config BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT + int "Telephone bearer count" + default 1 + range 1 $(UINT8_MAX) + help + The number of supported telephone bearers on the CCP Call Control Server + +module = BT_CCP_CALL_CONTROL_SERVER +module-str = "Call Control Profile Call Control Server" +source "subsys/logging/Kconfig.template.log_config" + +endif # BT_CCP_CALL_CONTROL_SERVER + +endif # BT_AUDIO diff --git a/subsys/bluetooth/audio/ccp_call_control_server.c b/subsys/bluetooth/audio/ccp_call_control_server.c new file mode 100644 index 00000000000000..a51354f4ce42dc --- /dev/null +++ b/subsys/bluetooth/audio/ccp_call_control_server.c @@ -0,0 +1,106 @@ +/* Bluetooth CCP - Call Control Profile Call Control Server + * + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(bt_ccp_call_control_server, CONFIG_BT_CCP_CALL_CONTROL_SERVER_LOG_LEVEL); + +/* A service instance can either be a GTBS or a TBS instance */ +struct bt_ccp_call_control_server_bearer { + uint8_t tbs_index; + bool registered; +}; + +static struct bt_ccp_call_control_server_bearer + bearers[CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT]; + +static struct bt_ccp_call_control_server_bearer *get_free_bearer(void) +{ + + for (size_t i = 0; i < ARRAY_SIZE(bearers); i++) { + if (!bearers[i].registered) { + return &bearers[i]; + } + } + + return NULL; +} + +int bt_ccp_call_control_server_register_bearer(const struct bt_tbs_register_param *param, + struct bt_ccp_call_control_server_bearer **bearer) +{ + struct bt_ccp_call_control_server_bearer *free_bearer; + int ret; + + CHECKIF(bearer == NULL) { + LOG_DBG("bearer is NULL"); + + return -EINVAL; + } + + free_bearer = get_free_bearer(); + if (free_bearer == NULL) { + return -ENOMEM; + } + + ret = bt_tbs_register_bearer(param); + if (ret < 0) { + LOG_DBG("Failed to register TBS bearer: %d", ret); + + /* Return known errors */ + if (ret == -EINVAL || ret == -EALREADY || ret == -EAGAIN || ret == -ENOMEM) { + return ret; + } + + return -ENOEXEC; + } + + free_bearer->registered = true; + free_bearer->tbs_index = (uint8_t)ret; + *bearer = free_bearer; + + return 0; +} + +int bt_ccp_call_control_server_unregister_bearer(struct bt_ccp_call_control_server_bearer *bearer) +{ + int err; + + CHECKIF(bearer == NULL) { + LOG_DBG("bearer is NULL"); + + return -EINVAL; + } + + if (!bearer->registered) { + LOG_DBG("Bearer %p already unregistered", bearer); + + return -EALREADY; + } + + err = bt_tbs_unregister_bearer(bearer->tbs_index); + if (err != 0) { + /* Return known errors */ + if (err == -EINVAL || err == -EALREADY) { + return err; + } + + return -ENOEXEC; + } + + bearer->registered = false; + + return 0; +} diff --git a/subsys/bluetooth/audio/shell/CMakeLists.txt b/subsys/bluetooth/audio/shell/CMakeLists.txt index d44f4e18417774..5fe6a71b615096 100644 --- a/subsys/bluetooth/audio/shell/CMakeLists.txt +++ b/subsys/bluetooth/audio/shell/CMakeLists.txt @@ -3,6 +3,10 @@ zephyr_library() zephyr_library_link_libraries(subsys__bluetooth) +zephyr_library_sources_ifdef( + CONFIG_BT_CCP_CALL_CONTROL_SERVER + ccp_call_control_server.c + ) zephyr_library_sources_ifdef( CONFIG_BT_VCP_VOL_REND vcp_vol_rend.c diff --git a/subsys/bluetooth/audio/shell/ccp_call_control_server.c b/subsys/bluetooth/audio/shell/ccp_call_control_server.c new file mode 100644 index 00000000000000..0d76cc5877a0a1 --- /dev/null +++ b/subsys/bluetooth/audio/shell/ccp_call_control_server.c @@ -0,0 +1,100 @@ +/** @file + * @brief Bluetooth Call Control Profile Call Control Server shell + */ + +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include + +#include +#include +#include +#include + +static struct bt_ccp_call_control_server_bearer + *bearers[CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT]; + +static int cmd_ccp_call_control_server_init(const struct shell *sh, size_t argc, char *argv[]) +{ + static bool registered; + + if (registered) { + shell_info(sh, "Already initialized"); + + return -ENOEXEC; + } + + const struct bt_tbs_register_param gtbs_param = { + .provider_name = "Generic TBS", + .uci = "un000", + .uri_schemes_supported = "tel,skype", + .gtbs = true, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = CONFIG_BT_TBS_SUPPORTED_FEATURES, + }; + int err; + + err = bt_ccp_call_control_server_register_bearer(>bs_param, &bearers[0]); + if (err != 0) { + shell_error(sh, "Failed to register GTBS bearer: %d", err); + + return -ENOEXEC; + } + + shell_info(sh, "Registered GTBS bearer"); + + for (int i = 1; i < CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT; i++) { + char prov_name[22]; /* Enough to store "Telephone Bearer #255" */ + const struct bt_tbs_register_param tbs_param = { + .provider_name = prov_name, + .uci = "un000", + .uri_schemes_supported = "tel,skype", + .gtbs = false, + .authorization_required = false, + /* Set different technologies per bearer */ + .technology = (i % BT_TBS_TECHNOLOGY_WCDMA) + 1, + .supported_features = CONFIG_BT_TBS_SUPPORTED_FEATURES, + }; + + snprintf(prov_name, sizeof(prov_name), "Telephone Bearer #%d", i); + + err = bt_ccp_call_control_server_register_bearer(&tbs_param, &bearers[i]); + if (err != 0) { + shell_error(sh, "Failed to register bearer[%d]: %d", i, err); + + return -ENOEXEC; + } + + shell_info(sh, "Registered bearer[%d]", i); + } + + registered = true; + + return 0; +} + +static int cmd_ccp_call_control_server(const struct shell *sh, size_t argc, char **argv) +{ + if (argc > 1) { + shell_error(sh, "%s unknown parameter: %s", argv[0], argv[1]); + } else { + shell_error(sh, "%s Missing subcommand", argv[0]); + } + + return -ENOEXEC; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(ccp_call_control_server_cmds, + SHELL_CMD_ARG(init, NULL, "Initialize CCP Call Control Server", + cmd_ccp_call_control_server_init, 1, 0), + SHELL_SUBCMD_SET_END); + +SHELL_CMD_ARG_REGISTER(ccp_call_control_server, &ccp_call_control_server_cmds, + "Bluetooth CCP Call Control Server shell commands", + cmd_ccp_call_control_server, 1, 1); diff --git a/tests/bluetooth/audio/ccp_call_control_server/CMakeLists.txt b/tests/bluetooth/audio/ccp_call_control_server/CMakeLists.txt new file mode 100644 index 00000000000000..40b476c4d66a32 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_server/CMakeLists.txt @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +project(bluetooth_ccp) +find_package(Zephyr COMPONENTS unittest HINTS $ENV{ZEPHYR_BASE}) + +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/audio/ccp_call_control_server/uut uut) + +target_link_libraries(testbinary PRIVATE uut) + +target_include_directories(testbinary PRIVATE include) + +target_sources(testbinary + PRIVATE + src/main.c +) diff --git a/tests/bluetooth/audio/ccp_call_control_server/prj.conf b/tests/bluetooth/audio/ccp_call_control_server/prj.conf new file mode 100644 index 00000000000000..e7d7406bf5f6a5 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_server/prj.conf @@ -0,0 +1,21 @@ +CONFIG_ZTEST=y + +CONFIG_BT=y +CONFIG_BT_SMP=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_GATT_DYNAMIC_DB=y +CONFIG_BT_AUDIO=y +CONFIG_BT_CCP_CALL_CONTROL_SERVER=y +CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT=2 +CONFIG_BT_TBS=y +CONFIG_BT_TBS_BEARER_COUNT=1 +CONFIG_UTF8=y + +CONFIG_ASSERT=y +CONFIG_ASSERT_LEVEL=2 +CONFIG_ASSERT_VERBOSE=y + +CONFIG_LOG=y +CONFIG_BT_CCP_CALL_CONTROL_SERVER_LOG_LEVEL_DBG=y +CONFIG_BT_TBS_LOG_LEVEL_DBG=y diff --git a/tests/bluetooth/audio/ccp_call_control_server/src/main.c b/tests/bluetooth/audio/ccp_call_control_server/src/main.c new file mode 100644 index 00000000000000..4d45690885f70b --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_server/src/main.c @@ -0,0 +1,264 @@ +/* main.c - Application main entry point */ + +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +DEFINE_FFF_GLOBALS; + +struct ccp_call_control_server_test_suite_fixture { + /** Need 1 additional bearer than the max to trigger some corner cases */ + struct bt_ccp_call_control_server_bearer + *bearers[CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT + 1]; +}; + +static void *ccp_call_control_server_test_suite_setup(void) +{ + struct ccp_call_control_server_test_suite_fixture *fixture; + + fixture = malloc(sizeof(*fixture)); + zassert_not_null(fixture); + + return fixture; +} + +static void ccp_call_control_server_test_suite_before(void *f) +{ + memset(f, 0, sizeof(struct ccp_call_control_server_test_suite_fixture)); +} + +static void ccp_call_control_server_test_suite_after(void *f) +{ + struct ccp_call_control_server_test_suite_fixture *fixture = f; + + /* We unregister from largest to lowest index, as GTBS shall be unregistered last and is + * always at index 0 + */ + for (size_t i = ARRAY_SIZE(fixture->bearers); i > 0; i--) { + /* Since size_t cannot be negative, we cannot use the regular i >= 0 when counting + * downwards as that will always be true, so we use an additional index variable + */ + const size_t index_to_unreg = i - 1; + + if (fixture->bearers[index_to_unreg] != NULL) { + bt_ccp_call_control_server_unregister_bearer( + fixture->bearers[index_to_unreg]); + } + + fixture->bearers[index_to_unreg] = NULL; + } +} + +static void ccp_call_control_server_test_suite_teardown(void *f) +{ + free(f); +} + +ZTEST_SUITE(ccp_call_control_server_test_suite, NULL, ccp_call_control_server_test_suite_setup, + ccp_call_control_server_test_suite_before, ccp_call_control_server_test_suite_after, + ccp_call_control_server_test_suite_teardown); + +static void register_default_bearer(struct ccp_call_control_server_test_suite_fixture *fixture) +{ + const struct bt_tbs_register_param register_param = { + .provider_name = "test", + .uci = "un999", + .uri_schemes_supported = "tel", + .gtbs = true, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = 0, + }; + int err; + + err = bt_ccp_call_control_server_register_bearer(®ister_param, &fixture->bearers[0]); + zassert_equal(err, 0, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, test_ccp_call_control_server_register_bearer) +{ + register_default_bearer(fixture); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_ccp_call_control_server_register_multiple_bearers) +{ + if (CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT == 1) { + ztest_test_skip(); + } + + register_default_bearer(fixture); + + for (int i = 1; i < CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT; i++) { + struct bt_tbs_register_param register_param = { + .provider_name = "test", + .uci = "un999", + .uri_schemes_supported = "tel", + .gtbs = false, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = 0, + }; + int err; + + err = bt_ccp_call_control_server_register_bearer(®ister_param, + &fixture->bearers[i]); + zassert_equal(err, 0, "Unexpected return value %d", err); + } +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_ccp_call_control_server_register_bearer_inval_null_param) +{ + int err; + + err = bt_ccp_call_control_server_register_bearer(NULL, &fixture->bearers[0]); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_ccp_call_control_server_register_bearer_inval_null_bearer) +{ + const struct bt_tbs_register_param register_param = { + .provider_name = "test", + .uci = "un999", + .uri_schemes_supported = "tel", + .gtbs = true, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = 0, + }; + int err; + + err = bt_ccp_call_control_server_register_bearer(®ister_param, NULL); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_ccp_call_control_server_register_bearer_inval_no_gtbs) +{ + const struct bt_tbs_register_param register_param = { + .provider_name = "test", + .uci = "un999", + .uri_schemes_supported = "tel", + .gtbs = false, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = 0, + }; + int err; + + err = bt_ccp_call_control_server_register_bearer(®ister_param, &fixture->bearers[0]); + zassert_equal(err, -EAGAIN, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_ccp_call_control_server_register_bearer_inval_double_gtbs) +{ + const struct bt_tbs_register_param register_param = { + .provider_name = "test", + .uci = "un999", + .uri_schemes_supported = "tel", + .gtbs = true, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = 0, + }; + int err; + + if (CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT == 1) { + ztest_test_skip(); + } + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_register_bearer(®ister_param, &fixture->bearers[1]); + zassert_equal(err, -EALREADY, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_ccp_call_control_server_register_bearer_inval_cnt) +{ + const struct bt_tbs_register_param register_param = { + .provider_name = "test", + .uci = "un999", + .uri_schemes_supported = "tel", + .gtbs = false, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = 0, + }; + int err; + + if (CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT == 1) { + ztest_test_skip(); + } + + register_default_bearer(fixture); + + for (int i = 1; i < CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT; i++) { + + err = bt_ccp_call_control_server_register_bearer(®ister_param, + &fixture->bearers[i]); + zassert_equal(err, 0, "Unexpected return value %d", err); + } + + err = bt_ccp_call_control_server_register_bearer( + ®ister_param, &fixture->bearers[CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT]); + zassert_equal(err, -ENOMEM, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, test_ccp_call_control_server_unregister_bearer) +{ + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_unregister_bearer(fixture->bearers[0]); + zassert_equal(err, 0, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_ccp_call_control_server_unregister_bearer_inval_double_unregister) +{ + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_unregister_bearer(fixture->bearers[0]); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_ccp_call_control_server_unregister_bearer(fixture->bearers[0]); + zassert_equal(err, -EALREADY, "Unexpected return value %d", err); + + fixture->bearers[0] = NULL; +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_ccp_call_control_server_unregister_bearer_inval_null_bearer) +{ + int err; + + err = bt_ccp_call_control_server_unregister_bearer(NULL); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} diff --git a/tests/bluetooth/audio/ccp_call_control_server/testcase.yaml b/tests/bluetooth/audio/ccp_call_control_server/testcase.yaml new file mode 100644 index 00000000000000..1a75b740147f29 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_server/testcase.yaml @@ -0,0 +1,7 @@ +common: + tags: + - bluetooth + - bluetooth_audio +tests: + bluetooth.audio.ccp_call_control_server.test: + type: unit diff --git a/tests/bluetooth/audio/ccp_call_control_server/uut/CMakeLists.txt b/tests/bluetooth/audio/ccp_call_control_server/uut/CMakeLists.txt new file mode 100644 index 00000000000000..6dd650947733bb --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_server/uut/CMakeLists.txt @@ -0,0 +1,22 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# +# CMakeLists.txt file for creating of uut library. +# + +add_library(uut STATIC + ${ZEPHYR_BASE}/subsys/bluetooth/audio/audio.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/ccid.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/ccp_call_control_server.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/tbs.c + ${ZEPHYR_BASE}/lib/net_buf/buf_simple.c + ${ZEPHYR_BASE}/lib/utils/utf8.c +) + +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/audio/mocks mocks) + +target_link_libraries(uut PUBLIC test_interface mocks) + +target_compile_options(uut PRIVATE -std=c11 -include ztest.h) diff --git a/tests/bluetooth/shell/audio.conf b/tests/bluetooth/shell/audio.conf index ea738538c4d3af..7b5a67d5b865fa 100644 --- a/tests/bluetooth/shell/audio.conf +++ b/tests/bluetooth/shell/audio.conf @@ -130,7 +130,9 @@ CONFIG_BT_MPL_MAX_OBJ_SIZE=600 CONFIG_BT_MPL_ICON_BITMAP_SIZE=321 CONFIG_BT_MPL_TRACK_MAX_SIZE=50 -# Telephone bearer service +# Call Control +CONFIG_BT_CCP_CALL_CONTROL_SERVER=y +CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT=2 CONFIG_BT_TBS=y CONFIG_BT_TBS_BEARER_COUNT=1 CONFIG_BT_TBS_SUPPORTED_FEATURES=3 diff --git a/tests/bluetooth/shell/testcase.yaml b/tests/bluetooth/shell/testcase.yaml index 6db24ccc0e51f6..6de8c815dc5326 100644 --- a/tests/bluetooth/shell/testcase.yaml +++ b/tests/bluetooth/shell/testcase.yaml @@ -312,6 +312,7 @@ tests: extra_args: CONF_FILE="audio.conf" build_only: true extra_configs: + - CONFIG_BT_CCP_CALL_CONTROL_SERVER=n - CONFIG_BT_TBS=n tags: bluetooth bluetooth.shell.audio.only_gtbs: @@ -419,3 +420,9 @@ tests: - nrf52_bsim integration_platforms: - nrf52_bsim + bluetooth.shell.audio.no_ccp_call_control_server: + extra_args: CONF_FILE="audio.conf" + build_only: true + extra_configs: + - CONFIG_BT_CCP_CALL_CONTROL_SERVER=n + tags: bluetooth diff --git a/tests/bluetooth/tester/overlay-le-audio.conf b/tests/bluetooth/tester/overlay-le-audio.conf index ef061eb1d55ecb..f9767590067a3e 100644 --- a/tests/bluetooth/tester/overlay-le-audio.conf +++ b/tests/bluetooth/tester/overlay-le-audio.conf @@ -125,6 +125,7 @@ CONFIG_BT_CSIP_SET_COORDINATOR=y CONFIG_BT_CSIP_SET_COORDINATOR_MAX_CSIS_INSTANCES=3 # CCP +CONFIG_BT_CCP_CALL_CONTROL_SERVER=y CONFIG_BT_ATT_TX_COUNT=12 CONFIG_BT_TBS_CLIENT_GTBS=y CONFIG_BT_TBS_CLIENT_TBS=y diff --git a/tests/bsim/bluetooth/audio/prj.conf b/tests/bsim/bluetooth/audio/prj.conf index 8c88146875a5bc..3cc57db36e94b0 100644 --- a/tests/bsim/bluetooth/audio/prj.conf +++ b/tests/bsim/bluetooth/audio/prj.conf @@ -89,7 +89,8 @@ CONFIG_BT_CSIP_SET_MEMBER_NOTIFIABLE=y CONFIG_BT_CSIP_SET_COORDINATOR=y CONFIG_BT_CSIP_SET_COORDINATOR_TEST_SAMPLE_DATA=y -# Telephone bearer service +# Call control +CONFIG_BT_CCP_CALL_CONTROL_SERVER=y CONFIG_BT_TBS=y CONFIG_BT_TBS_BEARER_COUNT=1 CONFIG_BT_TBS_CLIENT_TBS=y diff --git a/tests/bsim/bluetooth/audio/src/ccp_call_control_server_test.c b/tests/bsim/bluetooth/audio/src/ccp_call_control_server_test.c new file mode 100644 index 00000000000000..dfc6ef579531ee --- /dev/null +++ b/tests/bsim/bluetooth/audio/src/ccp_call_control_server_test.c @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "bstests.h" +#include "common.h" + +#ifdef CONFIG_BT_CCP_CALL_CONTROL_SERVER +LOG_MODULE_REGISTER(ccp_call_control_server, CONFIG_LOG_DEFAULT_LEVEL); + +extern enum bst_result_t bst_result; + +static struct bt_ccp_call_control_server_bearer + *bearers[CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT]; + +CREATE_FLAG(is_connected); +static void connected(struct bt_conn *conn, uint8_t err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (err != 0) { + FAIL("Failed to connect to %s (%u)\n", addr, err); + return; + } + + LOG_DBG("Connected to %s", addr); + + default_conn = bt_conn_ref(conn); + SET_FLAG(is_connected); +} + +static void init(void) +{ + static struct bt_conn_cb conn_callbacks = { + .connected = connected, + .disconnected = disconnected, + }; + + const struct bt_tbs_register_param gtbs_param = { + .provider_name = "Generic TBS", + .uci = "un000", + .uri_schemes_supported = "tel,skype", + .gtbs = true, + .authorization_required = false, + .technology = BT_TBS_TECHNOLOGY_3G, + .supported_features = CONFIG_BT_TBS_SUPPORTED_FEATURES, + }; + int err; + + err = bt_enable(NULL); + if (err != 0) { + FAIL("Bluetooth enable failed (err %d)\n", err); + + return; + } + + LOG_DBG("Bluetooth initialized"); + + err = bt_conn_cb_register(&conn_callbacks); + if (err != 0) { + FAIL("Failed to register conn CBs (err %d)\n", err); + + return; + } + + err = bt_le_scan_cb_register(&common_scan_cb); + if (err != 0) { + FAIL("Failed to register scan CBs (err %d)\n", err); + + return; + } + + err = bt_ccp_call_control_server_register_bearer(>bs_param, &bearers[0]); + if (err < 0) { + FAIL("Failed to register GTBS (err %d)\n", err); + + return; + } + + LOG_INF("Registered GTBS bearer"); + + for (int i = 1; i < CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT; i++) { + char prov_name[22]; /* Enough to store "Telephone Bearer #255" */ + const struct bt_tbs_register_param tbs_param = { + .provider_name = prov_name, + .uci = "un000", + .uri_schemes_supported = "tel,skype", + .gtbs = false, + .authorization_required = false, + /* Set different technologies per bearer */ + .technology = (i % BT_TBS_TECHNOLOGY_WCDMA) + 1, + .supported_features = CONFIG_BT_TBS_SUPPORTED_FEATURES, + }; + + snprintf(prov_name, sizeof(prov_name), "Telephone Bearer #%d", i); + + err = bt_ccp_call_control_server_register_bearer(&tbs_param, &bearers[i]); + if (err < 0) { + FAIL("Failed to register bearer[%d]: %d\n", i, err); + + return; + } + + LOG_INF("Registered bearer[%d]", i); + } +} + +static void unregister_bearers(void) +{ + for (int i = 0; i < CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT; i++) { + if (bearers[i] != NULL) { + const int err = bt_ccp_call_control_server_unregister_bearer(bearers[i]); + + if (err < 0) { + FAIL("Failed to unregister bearer[%d]: %d\n", i, err); + + return; + } + + LOG_DBG("Unregistered bearer[%d]", i); + + bearers[i] = NULL; + } + } +} + +static void test_main(void) +{ + init(); + + unregister_bearers(); + + PASS("CCP Call Control Server Passed\n"); +} + +static const struct bst_test_instance test_ccp_call_control_server[] = { + { + .test_id = "ccp_call_control_server", + .test_pre_init_f = test_init, + .test_tick_f = test_tick, + .test_main_f = test_main, + }, + BSTEST_END_MARKER, +}; + +struct bst_test_list *test_ccp_call_control_server_install(struct bst_test_list *tests) +{ + return bst_add_tests(tests, test_ccp_call_control_server); +} +#else +struct bst_test_list *test_ccp_call_control_server_install(struct bst_test_list *tests) +{ + return tests; +} + +#endif /* CONFIG_BT_TBS */ diff --git a/tests/bsim/bluetooth/audio/src/main.c b/tests/bsim/bluetooth/audio/src/main.c index 4490087eacc9c4..7fc13176bae9c1 100644 --- a/tests/bsim/bluetooth/audio/src/main.c +++ b/tests/bsim/bluetooth/audio/src/main.c @@ -44,6 +44,7 @@ extern struct bst_test_list *test_csip_notify_client_install(struct bst_test_lis extern struct bst_test_list *test_csip_notify_server_install(struct bst_test_list *tests); extern struct bst_test_list *test_gmap_ugg_install(struct bst_test_list *tests); extern struct bst_test_list *test_gmap_ugt_install(struct bst_test_list *tests); +extern struct bst_test_list *test_ccp_call_control_server_install(struct bst_test_list *tests); bst_test_install_t test_installers[] = { test_vcp_install, @@ -82,6 +83,7 @@ bst_test_install_t test_installers[] = { test_csip_notify_server_install, test_gmap_ugg_install, test_gmap_ugt_install, + test_ccp_call_control_server_install, NULL, }; diff --git a/tests/bsim/bluetooth/audio/test_scripts/ccp.sh b/tests/bsim/bluetooth/audio/test_scripts/ccp.sh new file mode 100755 index 00000000000000..cac742afbd84c8 --- /dev/null +++ b/tests/bsim/bluetooth/audio/test_scripts/ccp.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 + +source ${ZEPHYR_BASE}/tests/bsim/sh_common.source + +VERBOSITY_LEVEL=2 + +cd ${BSIM_OUT_PATH}/bin + +SIMULATION_ID="ccp" + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_prj_conf \ + -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} -d=0 -testid=ccp_call_control_server -rs=1 -D=1 + +# Simulation time should be larger than the WAIT_TIME in common.h +Execute ./bs_2G4_phy_v1 -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} \ + -D=1 -sim_length=60e6 $@ + +wait_for_background_jobs diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/CMakeLists.txt b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/CMakeLists.txt new file mode 100644 index 00000000000000..d7c38f708174a8 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/CMakeLists.txt @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ccp_call_control_server_self_tets) + +set(ccp_call_control_server_path ${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_server) + +target_sources(app PRIVATE + ${ccp_call_control_server_path}/src/main.c +) + +target_sources(app PRIVATE + src/test_main.c +) + +zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ + ) diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/Kconfig.sysbuild b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/Kconfig.sysbuild new file mode 100644 index 00000000000000..f2b0bb8c607e2b --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/Kconfig.sysbuild @@ -0,0 +1,10 @@ +# Copyright (c) 2023-2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_server/Kconfig.sysbuild" + +config NATIVE_SIMULATOR_PRIMARY_MCU_INDEX + int + # Let's pass the test arguments to the application MCU test + # otherwise by default they would have gone to the net core. + default 0 if $(BOARD_TARGET_STRING) = "NRF5340BSIM_NRF5340_CPUAPP" diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/prj.conf b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/prj.conf new file mode 100644 index 00000000000000..2e887d8812bbd6 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/prj.conf @@ -0,0 +1,2 @@ +# Please build using the sample configuration file: +# ${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_server/prj.conf diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/src/test_main.c b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/src/test_main.c new file mode 100644 index 00000000000000..d927d6f01cbcc9 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/src/test_main.c @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "bs_types.h" +#include "bs_tracing.h" +#include "bs_utils.h" +#include "bstests.h" + +#define WAIT_TIME 10 /* Seconds */ + +#define PASS_THRESHOLD 100 /* Audio packets */ + +extern enum bst_result_t bst_result; + +#define FAIL(...) \ + do { \ + bst_result = Failed; \ + bs_trace_error_time_line(__VA_ARGS__); \ + } while (0) + +#define PASS(...) \ + do { \ + bst_result = Passed; \ + bs_trace_info_time(1, __VA_ARGS__); \ + } while (0) + +static void test_ccp_call_control_server_sample_init(void) +{ + bst_ticker_set_next_tick_absolute(WAIT_TIME * 1e6); + bst_result = In_progress; +} + +static void test_ccp_call_control_server_sample_tick(bs_time_t HW_device_time) +{ + /* TODO: Once the sample is more complete we can expand the pass criteria */ + PASS("CCP Call Control Server sample PASSED\n"); +} + +static const struct bst_test_instance test_sample[] = { + { + .test_id = "ccp_call_control_server", + .test_descr = "Test based on the CCP Call Control Server sample. " + "It expects to be connected to a compatible CCP Call Control Client, " + "waits for " STR( + WAIT_TIME) " seconds, and checks how " + "many audio packets have been received correctly", + .test_post_init_f = test_ccp_call_control_server_sample_init, + .test_tick_f = test_ccp_call_control_server_sample_tick, + }, + BSTEST_END_MARKER, +}; + +static struct bst_test_list * +test_ccp_call_control_server_sample_install(struct bst_test_list *tests) +{ + tests = bst_add_tests(tests, test_sample); + return tests; +} + +bst_test_install_t test_installers[] = {test_ccp_call_control_server_sample_install, NULL}; diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/sysbuild.cmake b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/sysbuild.cmake new file mode 100644 index 00000000000000..4dc97dd85ac5fa --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_server/sysbuild.cmake @@ -0,0 +1,6 @@ +# Copyright (c) 2023-2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +include(${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_server/sysbuild.cmake) + +native_simulator_set_primary_mcu_index(${DEFAULT_IMAGE} ${NET_APP}) diff --git a/tests/bsim/bluetooth/audio_samples/ccp/compile.sh b/tests/bsim/bluetooth/audio_samples/ccp/compile.sh new file mode 100755 index 00000000000000..186805e7ec4169 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/compile.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Copyright 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +#set -x #uncomment this line for debugging +set -ue + +: "${ZEPHYR_BASE:?ZEPHYR_BASE must be set to point to the zephyr root directory}" + +source ${ZEPHYR_BASE}/tests/bsim/compile.source + +if [ "${BOARD_TS}" == "nrf5340bsim_nrf5340_cpuapp" ]; then + app=tests/bsim/bluetooth/audio_samples/ccp/call_control_server \ + sample=${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_server \ + conf_file=${sample}/prj.conf \ + conf_overlay=${sample}/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf \ + exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile +else + app=tests/bsim/bluetooth/audio_samples/ccp/call_control_server \ + sample=${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_server \ + conf_file=${sample}/prj.conf \ + conf_overlay=${sample}/overlay-bt_ll_sw_split.conf \ + exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile +fi + +wait_for_background_jobs diff --git a/tests/bsim/bluetooth/audio_samples/ccp/tests_scripts/ccp.sh b/tests/bsim/bluetooth/audio_samples/ccp/tests_scripts/ccp.sh new file mode 100755 index 00000000000000..87564607df7ab1 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/tests_scripts/ccp.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Copyright 2023-2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +# Simple selfchecking test for the CCP samples. + +simulation_id="ccp_samples_test" +verbosity_level=2 + +source ${ZEPHYR_BASE}/tests/bsim/sh_common.source + +cd ${BSIM_OUT_PATH}/bin + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_samples_ccp_call_control_server_prj_conf \ + -v=${verbosity_level} -s=${simulation_id} -d=0 -RealEncryption=1 -testid=ccp_call_control_server + +Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} \ + -D=1 -sim_length=20e6 $@ -argschannel -at=40 + +wait_for_background_jobs #Wait for all programs in background and return != 0 if any fails diff --git a/tests/bsim/bluetooth/audio_samples/compile.sh b/tests/bsim/bluetooth/audio_samples/compile.sh index cfc69b1605745c..f9f3300d2d40fa 100755 --- a/tests/bsim/bluetooth/audio_samples/compile.sh +++ b/tests/bsim/bluetooth/audio_samples/compile.sh @@ -14,5 +14,6 @@ source ${ZEPHYR_BASE}/tests/bsim/compile.source run_in_background ${ZEPHYR_BASE}/tests/bsim/bluetooth/audio_samples/bap_broadcast_sink/compile.sh run_in_background ${ZEPHYR_BASE}/tests/bsim/bluetooth/audio_samples/bap_unicast_client/compile.sh run_in_background ${ZEPHYR_BASE}/tests/bsim/bluetooth/audio_samples/cap/compile.sh +run_in_background ${ZEPHYR_BASE}/tests/bsim/bluetooth/audio_samples/ccp/compile.sh wait_for_background_jobs From d13c6f37e945261d49e00eccf1412d353204931a Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Thu, 15 Aug 2024 15:10:58 +0200 Subject: [PATCH 2/5] Bluetooth: CCP: Initial CCP Client implemenation Added initial CCP client implementation that simply does discovery of TBS on a remote CCP server. Signed-off-by: Emil Gydesen --- .../api/audio/bluetooth-le-audio-arch.rst | 4 +- .../bluetooth/shell/audio/ccp.rst | 25 ++ include/zephyr/bluetooth/audio/ccp.h | 80 +++++ .../ccp_call_control_client/CMakeLists.txt | 11 + .../ccp_call_control_client/Kconfig.sysbuild | 15 + .../ccp_call_control_client/README.rst | 78 ++++ .../nrf5340_audio_dk_nrf5340_cpuapp.conf | 6 + .../boards/nrf5340dk_nrf5340_cpuapp.conf | 6 + .../overlay-bt_ll_sw_split.conf | 6 + .../ccp_call_control_client/prj.conf | 22 ++ .../ccp_call_control_client/sample.yaml | 30 ++ .../ccp_call_control_client/src/main.c | 335 ++++++++++++++++++ .../ccp_call_control_client/sysbuild.cmake | 24 ++ subsys/bluetooth/audio/CMakeLists.txt | 1 + subsys/bluetooth/audio/Kconfig.ccp | 25 ++ .../bluetooth/audio/ccp_call_control_client.c | 234 ++++++++++++ subsys/bluetooth/audio/shell/CMakeLists.txt | 4 + .../audio/shell/ccp_call_control_client.c | 103 ++++++ subsys/bluetooth/audio/tbs_client.c | 1 - .../ccp_call_control_client/CMakeLists.txt | 17 + .../include/ccp_call_control_client.h | 25 ++ .../audio/ccp_call_control_client/prj.conf | 21 ++ .../audio/ccp_call_control_client/src/main.c | 212 +++++++++++ .../ccp_call_control_client/testcase.yaml | 7 + .../uut/CMakeLists.txt | 22 ++ .../uut/ccp_call_control_client.c | 31 ++ .../ccp_call_control_client/uut/tbs_client.c | 34 ++ tests/bluetooth/audio/mocks/include/conn.h | 4 + .../audio/mocks/include/expects_util.h | 4 + tests/bluetooth/audio/mocks/src/conn.c | 12 + tests/bluetooth/shell/audio.conf | 2 + tests/bluetooth/shell/testcase.yaml | 2 + tests/bsim/bluetooth/audio/prj.conf | 2 + .../audio/src/ccp_call_control_client_test.c | 133 +++++++ .../audio/src/ccp_call_control_server_test.c | 10 + tests/bsim/bluetooth/audio/src/main.c | 2 + .../bsim/bluetooth/audio/test_scripts/ccp.sh | 8 +- .../ccp/call_control_client/CMakeLists.txt | 23 ++ .../ccp/call_control_client/Kconfig.sysbuild | 10 + .../ccp/call_control_client/prj.conf | 2 + .../ccp/call_control_client/src/test_main.c | 71 ++++ .../ccp/call_control_client/sysbuild.cmake | 6 + .../bluetooth/audio_samples/ccp/compile.sh | 10 + .../audio_samples/ccp/tests_scripts/ccp.sh | 5 +- 44 files changed, 1678 insertions(+), 7 deletions(-) create mode 100644 samples/bluetooth/ccp_call_control_client/CMakeLists.txt create mode 100644 samples/bluetooth/ccp_call_control_client/Kconfig.sysbuild create mode 100644 samples/bluetooth/ccp_call_control_client/README.rst create mode 100644 samples/bluetooth/ccp_call_control_client/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf create mode 100644 samples/bluetooth/ccp_call_control_client/boards/nrf5340dk_nrf5340_cpuapp.conf create mode 100644 samples/bluetooth/ccp_call_control_client/overlay-bt_ll_sw_split.conf create mode 100644 samples/bluetooth/ccp_call_control_client/prj.conf create mode 100644 samples/bluetooth/ccp_call_control_client/sample.yaml create mode 100644 samples/bluetooth/ccp_call_control_client/src/main.c create mode 100644 samples/bluetooth/ccp_call_control_client/sysbuild.cmake create mode 100644 subsys/bluetooth/audio/ccp_call_control_client.c create mode 100644 subsys/bluetooth/audio/shell/ccp_call_control_client.c create mode 100644 tests/bluetooth/audio/ccp_call_control_client/CMakeLists.txt create mode 100644 tests/bluetooth/audio/ccp_call_control_client/include/ccp_call_control_client.h create mode 100644 tests/bluetooth/audio/ccp_call_control_client/prj.conf create mode 100644 tests/bluetooth/audio/ccp_call_control_client/src/main.c create mode 100644 tests/bluetooth/audio/ccp_call_control_client/testcase.yaml create mode 100644 tests/bluetooth/audio/ccp_call_control_client/uut/CMakeLists.txt create mode 100644 tests/bluetooth/audio/ccp_call_control_client/uut/ccp_call_control_client.c create mode 100644 tests/bluetooth/audio/ccp_call_control_client/uut/tbs_client.c create mode 100644 tests/bsim/bluetooth/audio/src/ccp_call_control_client_test.c create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_client/CMakeLists.txt create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_client/Kconfig.sysbuild create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_client/prj.conf create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_client/src/test_main.c create mode 100644 tests/bsim/bluetooth/audio_samples/ccp/call_control_client/sysbuild.cmake diff --git a/doc/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.rst b/doc/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.rst index 7eac2171a3280e..54274b2135f0be 100644 --- a/doc/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.rst +++ b/doc/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.rst @@ -726,8 +726,8 @@ Bluetooth Audio Stack. | | | | | - Shell Module | - Sample Application (in progress) | | | | | | - BSIM test | | | +-------------------------------+---------+------------------+-----------------------+--------------------------------------------------+ - | | Call Control Client | 1.0 | 3.0 | - Feature complete | - API refactor | - | | | | | - Shell Module | - Sample Application | + | | Call Control Client | 1.0 | 3.0 | - Feature complete | - API refactor (in progress) | + | | | | | - Shell Module | - Sample Application (in progress) | | | | | | - BSIM test | | +--------+-------------------------------+---------+------------------+-----------------------+--------------------------------------------------+ | MCP | Media Control Server | 1.0 | 3.0 | - Feature complete | - API refactor | diff --git a/doc/connectivity/bluetooth/shell/audio/ccp.rst b/doc/connectivity/bluetooth/shell/audio/ccp.rst index 1dc256a78befec..d25ea71d606f12 100644 --- a/doc/connectivity/bluetooth/shell/audio/ccp.rst +++ b/doc/connectivity/bluetooth/shell/audio/ccp.rst @@ -33,3 +33,28 @@ Setup Registered GTBS bearer Registered bearer[1] uart:~$ bt connect xx:xx:xx:xx:xx:xx public + +Call Control Client +******************* +The Call Control Client is a role that typically resides on resource contrained devices such as +earbuds or headsets. + +Using the Call Control Client +============================= +The Client can control a remote CCP server device. +For example a remote device may have an incoming call that can be accepted by the Client. + +.. code-block:: console + + uart:~$ ccp_call_control_client --help + ccp_call_control_client - Bluetooth CCP Call Control Client shell commands + Subcommands: + discover : Discover GTBS and TBS on remote device + +Example Usage when connected +============================ + +.. code-block:: console + + uart:~$ ccp_call_control_client discover + Discovery completed with GTBS and 1 TBS bearers diff --git a/include/zephyr/bluetooth/audio/ccp.h b/include/zephyr/bluetooth/audio/ccp.h index 4bd8b53fd8c72e..190d58313f272a 100644 --- a/include/zephyr/bluetooth/audio/ccp.h +++ b/include/zephyr/bluetooth/audio/ccp.h @@ -31,8 +31,12 @@ * The profile is not limited to carrier phone calls and can be used with common applications like * Discord and Teams. */ +#include +#include #include +#include +#include #ifdef __cplusplus extern "C" { @@ -90,6 +94,82 @@ int bt_ccp_call_control_server_unregister_bearer(struct bt_ccp_call_control_serv /** @} */ /* End of group bt_ccp_call_control_server */ +/** + * @defgroup bt_ccp_call_control_client CCP Call Control Client APIs + * @ingroup bt_ccp + * @{ + */ +/** Abstract Call Control Client structure. */ +struct bt_ccp_call_control_client; + +/** Abstract Call Control Client bearer structure. */ +struct bt_ccp_call_control_client_bearer; + +/** Struct with information about bearers of a client */ +struct bt_ccp_call_control_client_bearers { +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) + /** The GTBS bearer. */ + struct bt_ccp_call_control_client_bearer *gtbs_bearer; +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ + +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + /** Number of TBS bearers in @p tbs_bearers */ + size_t tbs_count; + + /** Array of pointers of TBS bearers */ + struct bt_ccp_call_control_client_bearer + *tbs_bearers[CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT]; +#endif /* CONFIG_BT_TBS_CLIENT_TBS */ +}; + +/** + * @brief Struct to hold the Telephone Bearer Service client callbacks + * + * These can be registered for usage with bt_tbs_client_register_cb(). + */ +struct bt_ccp_call_control_client_cb { + /** + * @brief Callback function for bt_ccp_call_control_client_discover(). + * + * This callback is called once the discovery procedure is completed. + * + * @param client Call Control Client pointer. + * @param err Error value. 0 on success, GATT error on positive + * value or errno on negative value. + * @param bearers The bearers found. + */ + void (*discover)(struct bt_ccp_call_control_client *client, int err, + struct bt_ccp_call_control_client_bearers *bearers); + + /** @internal Internally used field for list handling */ + sys_snode_t _node; +}; + +int bt_ccp_call_control_client_discover(struct bt_conn *conn, + struct bt_ccp_call_control_client **out_client); + +/** + * @brief Register callbacks for the Call Control Client + * + * @param cb The callback struct + * + * @retval 0 Succsss + * @retval -EINVAL @p cb is NULL + * @retval -EEXISTS @p cb is already registered + */ +int bt_ccp_call_control_client_register_cb(struct bt_ccp_call_control_client_cb *cb); + +/** + * @brief Unregister callbacks for the Call Control Client + * + * @param cb The callback struct + * + * @retval 0 Succsss + * @retval -EINVAL @p cb is NULL + * @retval -EALREADY @p cb is not registered + */ +int bt_ccp_call_control_client_unregister_cb(struct bt_ccp_call_control_client_cb *cb); +/** @} */ /* End of group bt_ccp_call_control_client */ #ifdef __cplusplus } #endif diff --git a/samples/bluetooth/ccp_call_control_client/CMakeLists.txt b/samples/bluetooth/ccp_call_control_client/CMakeLists.txt new file mode 100644 index 00000000000000..101c16bf0d60ba --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/CMakeLists.txt @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ccp_call_control_client) + +target_sources(app PRIVATE + src/main.c +) + +zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) diff --git a/samples/bluetooth/ccp_call_control_client/Kconfig.sysbuild b/samples/bluetooth/ccp_call_control_client/Kconfig.sysbuild new file mode 100644 index 00000000000000..f37b265ecbc27b --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/Kconfig.sysbuild @@ -0,0 +1,15 @@ +# Copyright 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "share/sysbuild/Kconfig" + +config NET_CORE_BOARD + string + default "nrf5340dk/nrf5340/cpunet" if "$(BOARD)" = "nrf5340dk" + default "nrf5340_audio_dk/nrf5340/cpunet" if "$(BOARD)" = "nrf5340_audio_dk" + default "nrf5340bsim/nrf5340/cpunet" if $(BOARD_TARGET_STRING) = "NRF5340BSIM_NRF5340_CPUAPP" + +config NET_CORE_IMAGE_HCI_IPC + bool "HCI IPC image on network core" + default y + depends on NET_CORE_BOARD != "" diff --git a/samples/bluetooth/ccp_call_control_client/README.rst b/samples/bluetooth/ccp_call_control_client/README.rst new file mode 100644 index 00000000000000..db2779297883bd --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/README.rst @@ -0,0 +1,78 @@ +.. zephyr:code-sample:: bluetooth_ccp_call_control_client + :name: Call Control Profile (CCP) Call Control Server + :relevant-api: bluetooth bt_ccp bt_tbs + + CCP Call Control Server sample that registers one or more TBS bearers and advertises the + TBS UUID(s). + +Overview +******** + +Application demonstrating the CCP Call Control Client functionality. +Starts by scanning for a CCP Call Control Server to connect and set up calls. + +The profile works for both GAP Central and GAP Peripheral devices, but this sample only assumes the +GAP Central role. + +This sample can be found under :zephyr_file:`samples/bluetooth/ccp_call_control_client` +in the Zephyr tree. + +Check the :zephyr:code-sample-category:`bluetooth` samples for general information. + +Requirements +************ + +* BlueZ running on the host, or +* A board with Bluetooth Low Energy 5.2 support + +Building and Running +******************** + +When building targeting an nrf52 series board with the Zephyr Bluetooth Controller, +use ``-DOVERLAY_CONFIG=overlay-bt_ll_sw_split.conf`` to enable the required feature support. + +Building for an nrf5340dk +------------------------- + +You can build both the application core image and an appropriate controller image for the network +core with: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/ccp_call_control_client/ + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +If you prefer to only build the application core image, you can do so by doing instead: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/ccp_call_control_client/ + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + +In that case you can pair this application core image with the +:zephyr:code-sample:`bluetooth_hci_ipc` sample +:zephyr_file:`samples/bluetooth/hci_ipc/nrf5340_cpunet_iso-bt_ll_sw_split.conf` configuration. + +Building for a simulated nrf5340bsim +------------------------------------ + +Similarly to how you would for real HW, you can do: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/ccp_call_control_client/ + :board: nrf5340bsim/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +Note this will produce a Linux executable in :file:`./build/zephyr/zephyr.exe`. +For more information, check :ref:`this board documentation `. + +Building for a simulated nrf52_bsim +----------------------------------- + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/ccp_call_control_client/ + :board: nrf52_bsim + :goals: build + :gen-args: -DOVERLAY_CONFIG=overlay-bt_ll_sw_split.conf diff --git a/samples/bluetooth/ccp_call_control_client/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf b/samples/bluetooth/ccp_call_control_client/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf new file mode 100644 index 00000000000000..f58eedb0453c57 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf @@ -0,0 +1,6 @@ +CONFIG_BT_BUF_EVT_RX_SIZE=255 +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 + +CONFIG_BT_SEND_ECC_EMULATION=y diff --git a/samples/bluetooth/ccp_call_control_client/boards/nrf5340dk_nrf5340_cpuapp.conf b/samples/bluetooth/ccp_call_control_client/boards/nrf5340dk_nrf5340_cpuapp.conf new file mode 100644 index 00000000000000..f58eedb0453c57 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/boards/nrf5340dk_nrf5340_cpuapp.conf @@ -0,0 +1,6 @@ +CONFIG_BT_BUF_EVT_RX_SIZE=255 +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 + +CONFIG_BT_SEND_ECC_EMULATION=y diff --git a/samples/bluetooth/ccp_call_control_client/overlay-bt_ll_sw_split.conf b/samples/bluetooth/ccp_call_control_client/overlay-bt_ll_sw_split.conf new file mode 100644 index 00000000000000..dce0f2c298ec13 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/overlay-bt_ll_sw_split.conf @@ -0,0 +1,6 @@ +# Zephyr Bluetooth Controller +CONFIG_BT_LL_SW_SPLIT=y + +# Zephyr Controller tested maximum advertising data that can be set in a single HCI command +CONFIG_BT_CTLR_ADV_DATA_LEN_MAX=191 +CONFIG_BT_CTLR_SCAN_DATA_LEN_MAX=191 diff --git a/samples/bluetooth/ccp_call_control_client/prj.conf b/samples/bluetooth/ccp_call_control_client/prj.conf new file mode 100644 index 00000000000000..85549f2c321c70 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/prj.conf @@ -0,0 +1,22 @@ +CONFIG_BT=y +CONFIG_LOG=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_GATT_AUTO_DISCOVER_CCC=y +CONFIG_BT_AUDIO=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_DEVICE_NAME="CCP Call Control Client" + +CONFIG_BT_SMP=y +CONFIG_BT_KEYS_OVERWRITE_OLDEST=y + +# CCP support +CONFIG_BT_CCP_CALL_CONTROL_CLIENT=y +CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT=2 +CONFIG_BT_TBS_CLIENT_GTBS=y +CONFIG_BT_TBS_CLIENT_TBS=y +CONFIG_BT_TBS_CLIENT_MAX_TBS_INSTANCES=1 +CONFIG_UTF8=y + +# TBS Client may require up to 12 buffers +CONFIG_BT_ATT_TX_COUNT=12 diff --git a/samples/bluetooth/ccp_call_control_client/sample.yaml b/samples/bluetooth/ccp_call_control_client/sample.yaml new file mode 100644 index 00000000000000..88fc3e30aa1017 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/sample.yaml @@ -0,0 +1,30 @@ +sample: + description: Bluetooth Low Energy Call Control Profile Server sample + name: Bluetooth Low Energy Call Control Profile Server sample +tests: + sample.bluetooth.ccp_call_control_client: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf5340dk/nrf5340/cpuapp + - nrf5340bsim/nrf5340/cpuapp + integration_platforms: + - qemu_x86 + - nrf5340dk/nrf5340/cpuapp + tags: bluetooth + sysbuild: true + sample.bluetooth.ccp_call_control_client.bt_ll_sw_split: + harness: bluetooth + platform_allow: + - nrf52_bsim + - nrf52833dk/nrf52833 + - nrf52840dk/nrf52840 + - nrf52840dongle/nrf52840 + integration_platforms: + - nrf52_bsim + - nrf52833dk/nrf52833 + - nrf52840dk/nrf52840 + - nrf52840dongle/nrf52840 + extra_args: OVERLAY_CONFIG=overlay-bt_ll_sw_split.conf + tags: bluetooth diff --git a/samples/bluetooth/ccp_call_control_client/src/main.c b/samples/bluetooth/ccp_call_control_client/src/main.c new file mode 100644 index 00000000000000..db8341204a47f4 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/src/main.c @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(ccp_call_control_client, CONFIG_LOG_DEFAULT_LEVEL); + +#define SEM_TIMEOUT K_SECONDS(10) + +static struct bt_conn *peer_conn; +/* client is not static as it is used for testing purposes */ +struct bt_ccp_call_control_client *client; +static struct bt_ccp_call_control_client_bearers client_bearers; + +static K_SEM_DEFINE(sem_conn_state_change, 0, 1); +static K_SEM_DEFINE(sem_security_updated, 0, 1); +static K_SEM_DEFINE(sem_ccp_action_completed, 0, 1); + +static void connected_cb(struct bt_conn *conn, uint8_t err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + LOG_INF("Connected: %s", addr); + + k_sem_give(&sem_conn_state_change); +} + +static void disconnected_cb(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + if (conn != peer_conn) { + return; + } + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + LOG_INF("Disconnected: %s (reason 0x%02x)", addr, reason); + + bt_conn_unref(peer_conn); + peer_conn = NULL; + client = NULL; + memset(&client_bearers, 0, sizeof(client_bearers)); + k_sem_give(&sem_conn_state_change); +} + +static void security_changed_cb(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) +{ + if (err == 0) { + k_sem_give(&sem_security_updated); + } else { + LOG_ERR("Failed to set security level: %s(%u)", bt_security_err_to_str(err), err); + } +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected_cb, + .disconnected = disconnected_cb, + .security_changed = security_changed_cb, +}; + +static bool check_gtbs_support(struct bt_data *data, void *user_data) +{ + struct net_buf_simple svc_data; + bool *connect = user_data; + const struct bt_uuid *uuid; + uint16_t uuid_val; + + if (data->type != BT_DATA_SVC_DATA16) { + return true; /* Continue parsing to next AD data type */ + } + + if (data->data_len < sizeof(uuid_val)) { + LOG_WRN("AD invalid size %u", data->data_len); + return true; /* Continue parsing to next AD data type */ + } + + net_buf_simple_init_with_data(&svc_data, (void *)data->data, data->data_len); + + /* Pull the 16-bit service data and compare to what we are searching for */ + uuid_val = net_buf_simple_pull_le16(&svc_data); + uuid = BT_UUID_DECLARE_16(sys_le16_to_cpu(uuid_val)); + if (bt_uuid_cmp(uuid, BT_UUID_GTBS) != 0) { + /* We are looking for the GTBS service data */ + return true; /* Continue parsing to next AD data type */ + } + + *connect = true; + + return false; /* Stop parsing */ +} + +static void scan_recv_cb(const struct bt_le_scan_recv_info *info, struct net_buf_simple *ad) +{ + char addr_str[BT_ADDR_LE_STR_LEN]; + bool connect = false; + + if (peer_conn != NULL) { + /* Already connected */ + return; + } + + /* CCP mandates that connectbale extended advertising is used by the peripherals so we + * ignore any scan report this is not that. + * We also ignore reports with poor RSSI + */ + if (info->adv_type != BT_GAP_ADV_TYPE_EXT_ADV || + (info->adv_props & BT_GAP_ADV_PROP_EXT_ADV) == 0 || + (info->adv_props & BT_GAP_ADV_PROP_CONNECTABLE) == 0 || info->rssi < -70) { + return; + } + + (void)bt_addr_le_to_str(info->addr, addr_str, sizeof(addr_str)); + LOG_INF("Connectable device found: %s (RSSI %d)", addr_str, info->rssi); + + /* Iterate on the advertising data to see if claims GTBS support */ + bt_data_parse(ad, check_gtbs_support, &connect); + + if (connect) { + int err; + + err = bt_le_scan_stop(); + if (err != 0) { + LOG_ERR("Scanning failed to stop (err %d)", err); + return; + } + + LOG_INF("Connecting to found CCP server"); + err = bt_conn_le_create(info->addr, BT_CONN_LE_CREATE_CONN, + BT_LE_CONN_PARAM_DEFAULT, &peer_conn); + if (err != 0) { + LOG_ERR("Conn create failed: %d", err); + } + } +} + +static int scan_and_connect(void) +{ + int err; + + err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, NULL); + if (err != 0) { + LOG_ERR("Scanning failed to start (err %d)", err); + return err; + } + + LOG_INF("Scanning successfully started"); + + err = k_sem_take(&sem_conn_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("failed to take sem_connected (err %d)", err); + return err; + } + + err = bt_conn_set_security(peer_conn, BT_SECURITY_L2); + if (err != 0) { + LOG_ERR("failed to set security (err %d)", err); + return err; + } + + err = k_sem_take(&sem_security_updated, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("failed to take sem_security_updated (err %d)", err); + return err; + } + + LOG_INF("Security successfully updated"); + + return 0; +} + +static void ccp_call_control_client_discover_cb(struct bt_ccp_call_control_client *client, int err, + struct bt_ccp_call_control_client_bearers *bearers) +{ + if (err != 0) { + LOG_ERR("Discovery failed: %d", err); + return; + } + + LOG_INF("Discovery completed with %s%u TBS bearers", + bearers->gtbs_bearer != NULL ? "GTBS and " : "", bearers->tbs_count); + + memcpy(&client_bearers, bearers, sizeof(client_bearers)); + + k_sem_give(&sem_ccp_action_completed); +} + +static int reset_ccp_call_control_client(void) +{ + int err; + + LOG_INF("Resetting"); + + if (peer_conn != NULL) { + err = bt_conn_disconnect(peer_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err != 0) { + return err; + } + + err = k_sem_take(&sem_conn_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_conn_state_change: %d", err); + return err; + } + } + + /* If scanning is already stopped it will still return `0` */ + err = bt_le_scan_stop(); + if (err != 0) { + LOG_ERR("Scanning failed to stop (err %d)", err); + return err; + } + + k_sem_reset(&sem_conn_state_change); + + return 0; +} + +static int discover_services(void) +{ + int err; + + LOG_INF("Discovering GTBS and TBS"); + + err = bt_ccp_call_control_client_discover(peer_conn, &client); + if (err != 0) { + LOG_ERR("Failed to discover: %d", err); + return err; + } + + err = k_sem_take(&sem_ccp_action_completed, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Failed to take sem_ccp_action_completed: %d", err); + return err; + } + + return 0; +} + +static int init_ccp_call_control_client(void) +{ + static struct bt_le_scan_cb scan_cbs = { + .recv = scan_recv_cb, + }; + static struct bt_ccp_call_control_client_cb ccp_call_control_client_cbs = { + .discover = ccp_call_control_client_discover_cb, + }; + int err; + + err = bt_enable(NULL); + if (err != 0) { + LOG_ERR("Bluetooth enable failed (err %d)", err); + + return err; + } + + LOG_DBG("Bluetooth initialized"); + err = bt_le_scan_cb_register(&scan_cbs); + if (err != 0) { + LOG_ERR("Bluetooth enable failed (err %d)", err); + + return err; + } + + err = bt_ccp_call_control_client_register_cb(&ccp_call_control_client_cbs); + if (err != 0) { + LOG_ERR("Bluetooth enable failed (err %d)", err); + + return err; + } + + return 0; +} + +int main(void) +{ + int err; + + err = init_ccp_call_control_client(); + if (err != 0) { + return 0; + } + + LOG_INF("CCP Call Control Client initialized"); + + while (true) { + err = reset_ccp_call_control_client(); + if (err != 0) { + break; + } + + /* Start scanning for CCP servers and connect to the first we find */ + err = scan_and_connect(); + if (err != 0) { + continue; + } + + /* Discover TBS and GTBS on the remove server */ + err = discover_services(); + if (err != 0) { + continue; + } + + /* Reset if disconnected */ + err = k_sem_take(&sem_conn_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_conn_state_change: err %d", err); + break; + } + } + + return 0; +} diff --git a/samples/bluetooth/ccp_call_control_client/sysbuild.cmake b/samples/bluetooth/ccp_call_control_client/sysbuild.cmake new file mode 100644 index 00000000000000..2523aac8ea76f1 --- /dev/null +++ b/samples/bluetooth/ccp_call_control_client/sysbuild.cmake @@ -0,0 +1,24 @@ +# Copyright (c) 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +if(SB_CONFIG_NET_CORE_IMAGE_HCI_IPC) + # For builds in the nrf5340, we build the netcore image with the controller + + set(NET_APP hci_ipc) + set(NET_APP_SRC_DIR ${ZEPHYR_BASE}/samples/bluetooth/${NET_APP}) + + ExternalZephyrProject_Add( + APPLICATION ${NET_APP} + SOURCE_DIR ${NET_APP_SRC_DIR} + BOARD ${SB_CONFIG_NET_CORE_BOARD} + ) + + set(${NET_APP}_CONF_FILE + ${NET_APP_SRC_DIR}/nrf5340_cpunet_iso-bt_ll_sw_split.conf + CACHE INTERNAL "" + ) + + native_simulator_set_child_images(${DEFAULT_IMAGE} ${NET_APP}) +endif() + +native_simulator_set_final_executable(${DEFAULT_IMAGE}) diff --git a/subsys/bluetooth/audio/CMakeLists.txt b/subsys/bluetooth/audio/CMakeLists.txt index 86f507746be2db..a7537140a64e53 100644 --- a/subsys/bluetooth/audio/CMakeLists.txt +++ b/subsys/bluetooth/audio/CMakeLists.txt @@ -58,6 +58,7 @@ zephyr_library_sources_ifdef(CONFIG_BT_BAP_BROADCAST_SOURCE bap_broadcast_source zephyr_library_sources_ifdef(CONFIG_BT_BAP_BROADCAST_SINK bap_broadcast_sink.c) zephyr_library_sources_ifdef(CONFIG_BT_BAP_SCAN_DELEGATOR bap_scan_delegator.c) zephyr_library_sources_ifdef(CONFIG_BT_BAP_BROADCAST_ASSISTANT bap_broadcast_assistant.c) +zephyr_library_sources_ifdef(CONFIG_BT_CCP_CALL_CONTROL_CLIENT ccp_call_control_client.c) zephyr_library_sources_ifdef(CONFIG_BT_CCP_CALL_CONTROL_SERVER ccp_call_control_server.c) zephyr_library_sources_ifdef(CONFIG_BT_HAS has.c) zephyr_library_sources_ifdef(CONFIG_BT_HAS_CLIENT has_client.c) diff --git a/subsys/bluetooth/audio/Kconfig.ccp b/subsys/bluetooth/audio/Kconfig.ccp index f43557e37a8699..c397b831cfc01b 100644 --- a/subsys/bluetooth/audio/Kconfig.ccp +++ b/subsys/bluetooth/audio/Kconfig.ccp @@ -7,6 +7,31 @@ if BT_AUDIO +config BT_CCP_CALL_CONTROL_CLIENT + bool "Call Control Profile Client Support" + depends on BT_EXT_ADV + depends on BT_TBS_CLIENT + depends on BT_BONDABLE + help + This option enables support for the Call Control Profile Client which uses the Telephone + Bearer Service (TBS) client to control calls on a remote device. + +if BT_CCP_CALL_CONTROL_CLIENT + +config BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT + int "Telephone bearer count" + default 1 + range 1 $(UINT8_MAX) if BT_TBS_CLIENT_TBS + range 1 1 + help + The number of supported telephone bearers on the CCP Call Control Client + +module = BT_CCP_CALL_CONTROL_CLIENT +module-str = "Call Control Profile Client" +source "subsys/logging/Kconfig.template.log_config" + +endif # BT_CCP_CALL_CONTROL_CLIENT + config BT_CCP_CALL_CONTROL_SERVER bool "Call Control Profile Call Control Server Support" depends on BT_EXT_ADV diff --git a/subsys/bluetooth/audio/ccp_call_control_client.c b/subsys/bluetooth/audio/ccp_call_control_client.c new file mode 100644 index 00000000000000..641f887c3f483a --- /dev/null +++ b/subsys/bluetooth/audio/ccp_call_control_client.c @@ -0,0 +1,234 @@ +/* Bluetooth CCP - Call Control Profile Call Control Server + * + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(bt_ccp_call_control_client, CONFIG_BT_CCP_CALL_CONTROL_CLIENT_LOG_LEVEL); + +static sys_slist_t ccp_call_control_client_cbs = + SYS_SLIST_STATIC_INIT(&ccp_call_control_client_cbs); + +static struct bt_tbs_client_cb tbs_client_cbs; + +/* A service instance can either be a GTBS or a TBS insttance */ +struct bt_ccp_call_control_client_bearer { + uint8_t tbs_index; + bool discovered; +}; + +enum ccp_call_control_client_flag { + CCP_CALL_CONTROL_CLIENT_FLAG_BUSY, + + CCP_CALL_CONTROL_CLIENT_FLAG_NUM_FLAGS, /* keep as last */ +}; + +struct bt_ccp_call_control_client { + struct bt_ccp_call_control_client_bearer + bearers[CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT]; + struct bt_conn *conn; + + ATOMIC_DEFINE(flags, CCP_CALL_CONTROL_CLIENT_FLAG_NUM_FLAGS); +}; + +static struct bt_ccp_call_control_client clients[CONFIG_BT_MAX_CONN]; + +static struct bt_ccp_call_control_client *get_client_by_conn(const struct bt_conn *conn) +{ + return &clients[bt_conn_index(conn)]; +} + +static void connected_cb(struct bt_conn *conn, uint8_t err) +{ + static bool cbs_registered; + + /* We register the callbacks in the connected callback. That way we ensure that they are + * registered before any procedures are completed or we receive any notifications, while + * registering them as late as possible + */ + if (err == BT_HCI_ERR_SUCCESS && !cbs_registered) { + int cb_err; + + cb_err = bt_tbs_client_register_cb(&tbs_client_cbs); + __ASSERT(cb_err == 0, "Failed to register TBS callbacks: %d", cb_err); + + cbs_registered = true; + } +} + +static void disconnected_cb(struct bt_conn *conn, uint8_t reason) +{ + struct bt_ccp_call_control_client *client = get_client_by_conn(conn); + + /* client->conn may be NULL */ + if (client->conn == conn) { + bt_conn_unref(client->conn); + client->conn = NULL; + } +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected_cb, + .disconnected = disconnected_cb, +}; + +static void populate_bearers(struct bt_ccp_call_control_client *client, + struct bt_ccp_call_control_client_bearers *bearers) +{ + size_t i = 0; + +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) + if (client->bearers[i].discovered) { + bearers->gtbs_bearer = &client->bearers[i++]; + } +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ + +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + for (; i < ARRAY_SIZE(client->bearers); i++) { + if (!client->bearers[i].discovered) { + break; + } + + bearers->tbs_bearers[bearers->tbs_count++] = &client->bearers[i]; + } +#endif /* CONFIG_BT_TBS_CLIENT_TBS */ +} + +static void tbs_client_discover_cb(struct bt_conn *conn, int err, uint8_t tbs_count, + bool gtbs_found) +{ + struct bt_ccp_call_control_client *client = get_client_by_conn(conn); + struct bt_ccp_call_control_client_bearers bearers = {0}; + struct bt_ccp_call_control_client_cb *listener, *next; + + LOG_DBG("conn %p err %d tbs_count %u gtbs_found %d", (void *)conn, err, tbs_count, + gtbs_found); + + memset(client->bearers, 0, sizeof((client->bearers))); + + if (IS_ENABLED(CONFIG_BT_TBS_CLIENT_GTBS) && gtbs_found) { + client->bearers[0].discovered = true; + client->bearers[0].tbs_index = BT_TBS_GTBS_INDEX; + } + + if (IS_ENABLED(CONFIG_BT_TBS_CLIENT_TBS)) { + for (uint8_t i = 0U; i < tbs_count; i++) { + const uint8_t idx = i + (gtbs_found ? 1 : 0); + + if (idx >= ARRAY_SIZE(client->bearers)) { + LOG_WRN("Discoverd more TBS instances (%u) than the CCP Call " + "Control Client supports %zu", + tbs_count, ARRAY_SIZE(client->bearers)); + break; + } + + client->bearers[idx].discovered = true; + client->bearers[idx].tbs_index = i; + } + } + + populate_bearers(client, &bearers); + + atomic_clear_bit(client->flags, CCP_CALL_CONTROL_CLIENT_FLAG_BUSY); + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&ccp_call_control_client_cbs, listener, next, _node) { + if (listener->discover != NULL) { + listener->discover(client, err, &bearers); + } + } +} + +int bt_ccp_call_control_client_discover(struct bt_conn *conn, + struct bt_ccp_call_control_client **out_client) +{ + struct bt_ccp_call_control_client *client; + int err; + + CHECKIF(conn == NULL) { + LOG_DBG("conn is NULL"); + + return -EINVAL; + } + + CHECKIF(out_client == NULL) { + LOG_DBG("client is NULL"); + + return -EINVAL; + } + + client = get_client_by_conn(conn); + if (atomic_test_and_set_bit(client->flags, CCP_CALL_CONTROL_CLIENT_FLAG_BUSY)) { + return -EBUSY; + } + + tbs_client_cbs.discover = tbs_client_discover_cb; + + err = bt_tbs_client_discover(conn); + if (err != 0) { + LOG_DBG("Failed to discover TBS for %p: %d", (void *)conn, err); + + atomic_clear_bit(client->flags, CCP_CALL_CONTROL_CLIENT_FLAG_BUSY); + + /* Return known errors */ + if (err == -EBUSY) { + return err; + } + + return -ENOEXEC; + } + + client->conn = bt_conn_ref(conn); + *out_client = client; + + return 0; +} + +int bt_ccp_call_control_client_register_cb(struct bt_ccp_call_control_client_cb *cb) +{ + CHECKIF(cb == NULL) { + LOG_DBG("cb is NULL"); + + return -EINVAL; + } + + if (sys_slist_find(&ccp_call_control_client_cbs, &cb->_node, NULL)) { + return -EEXIST; + } + + sys_slist_append(&ccp_call_control_client_cbs, &cb->_node); + + return 0; +} + +int bt_ccp_call_control_client_unregister_cb(struct bt_ccp_call_control_client_cb *cb) +{ + CHECKIF(cb == NULL) { + LOG_DBG("cb is NULL"); + return -EINVAL; + } + + if (!sys_slist_find_and_remove(&ccp_call_control_client_cbs, &cb->_node)) { + return -EALREADY; + } + + return 0; +} diff --git a/subsys/bluetooth/audio/shell/CMakeLists.txt b/subsys/bluetooth/audio/shell/CMakeLists.txt index 5fe6a71b615096..bb71e1e65c0ae5 100644 --- a/subsys/bluetooth/audio/shell/CMakeLists.txt +++ b/subsys/bluetooth/audio/shell/CMakeLists.txt @@ -7,6 +7,10 @@ zephyr_library_sources_ifdef( CONFIG_BT_CCP_CALL_CONTROL_SERVER ccp_call_control_server.c ) +zephyr_library_sources_ifdef( + CONFIG_BT_CCP_CALL_CONTROL_CLIENT + ccp_call_control_client.c + ) zephyr_library_sources_ifdef( CONFIG_BT_VCP_VOL_REND vcp_vol_rend.c diff --git a/subsys/bluetooth/audio/shell/ccp_call_control_client.c b/subsys/bluetooth/audio/shell/ccp_call_control_client.c new file mode 100644 index 00000000000000..c2c57e1bfb43e0 --- /dev/null +++ b/subsys/bluetooth/audio/shell/ccp_call_control_client.c @@ -0,0 +1,103 @@ +/** @file + * @brief Bluetooth Call Control Profile Call Control Client shell + */ + +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "host/shell/bt.h" + +static struct bt_ccp_call_control_client *clients[CONFIG_BT_MAX_CONN]; + +static void ccp_call_control_client_discover_cb(struct bt_ccp_call_control_client *client, int err, + struct bt_ccp_call_control_client_bearers *bearers) +{ + struct bt_ccp_call_control_client_bearer *gtbs_bearer = NULL; + uint8_t tbs_count = 0U; + + if (err != 0) { + shell_error(ctx_shell, "Failed to discover TBS: %d", err); + return; + } +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) + gtbs_bearer = bearers->gtbs_bearer; +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + tbs_count = bearers->tbs_count; +#endif /* CONFIG_BT_TBS_CLIENT_TBS */ + + shell_info(ctx_shell, "Discovery completed with %s%u TBS bearers", + gtbs_bearer != NULL ? "GTBS and " : "", tbs_count); +} + +static int cmd_ccp_call_control_client_discover(const struct shell *sh, size_t argc, char *argv[]) +{ + static bool cb_registered; + + int err; + + if (default_conn == NULL) { + shell_error(sh, "Not connected"); + return -ENOEXEC; + } + + if (ctx_shell == NULL) { + ctx_shell = sh; + } + + if (!cb_registered) { + static struct bt_ccp_call_control_client_cb ccp_call_control_client_cbs = { + .discover = ccp_call_control_client_discover_cb, + }; + + err = bt_ccp_call_control_client_register_cb(&ccp_call_control_client_cbs); + if (err != 0) { + shell_error(sh, "Failed to register CCP Call Control Client cbs (err %d)", + err); + return -ENOEXEC; + } + } + + err = bt_ccp_call_control_client_discover(default_conn, + &clients[bt_conn_index(default_conn)]); + if (err != 0) { + shell_error(sh, "Failed to discover GTBS: %d", err); + + return -ENOEXEC; + } + + return 0; +} + +static int cmd_ccp_call_control_client(const struct shell *sh, size_t argc, char **argv) +{ + if (argc > 1) { + shell_error(sh, "%s unknown parameter: %s", argv[0], argv[1]); + } else { + shell_error(sh, "%s Missing subcommand", argv[0]); + } + + return -ENOEXEC; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(ccp_call_control_client_cmds, + SHELL_CMD_ARG(discover, NULL, + "Discover GTBS and TBS on remote device", + cmd_ccp_call_control_client_discover, 1, 0), + SHELL_SUBCMD_SET_END); + +SHELL_CMD_ARG_REGISTER(ccp_call_control_client, &ccp_call_control_client_cmds, + "Bluetooth CCP Call Control Client shell commands", + cmd_ccp_call_control_client, 1, 1); diff --git a/subsys/bluetooth/audio/tbs_client.c b/subsys/bluetooth/audio/tbs_client.c index db8e081f83ec35..f6e633d38e7055 100644 --- a/subsys/bluetooth/audio/tbs_client.c +++ b/subsys/bluetooth/audio/tbs_client.c @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include diff --git a/tests/bluetooth/audio/ccp_call_control_client/CMakeLists.txt b/tests/bluetooth/audio/ccp_call_control_client/CMakeLists.txt new file mode 100644 index 00000000000000..05e846d7878e25 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/CMakeLists.txt @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +project(bluetooth_ccp) +find_package(Zephyr COMPONENTS unittest HINTS $ENV{ZEPHYR_BASE}) + +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/audio/ccp_call_control_client/uut uut) + +target_link_libraries(testbinary PRIVATE uut) + +target_include_directories(testbinary PRIVATE include) + +target_sources(testbinary + PRIVATE + src/main.c +) diff --git a/tests/bluetooth/audio/ccp_call_control_client/include/ccp_call_control_client.h b/tests/bluetooth/audio/ccp_call_control_client/include/ccp_call_control_client.h new file mode 100644 index 00000000000000..998c9f99852bb1 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/include/ccp_call_control_client.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef MOCKS_CCP_CALL_CONTROL_CLIENT_H_ +#define MOCKS_CCP_CALL_CONTROL_CLIENT_H_ + +#include + +#include +#include +#include + +extern struct bt_ccp_call_control_client_cb mock_ccp_call_control_client_cb; + +void mock_ccp_call_control_client_init(void); +void mock_ccp_call_control_client_cleanup(void); + +DECLARE_FAKE_VOID_FUNC(mock_ccp_call_control_client_discover_cb, + struct bt_ccp_call_control_client *, int, + struct bt_ccp_call_control_client_bearers *); + +#endif /* MOCKS_CCP_CALL_CONTROL_CLIENT_H_ */ diff --git a/tests/bluetooth/audio/ccp_call_control_client/prj.conf b/tests/bluetooth/audio/ccp_call_control_client/prj.conf new file mode 100644 index 00000000000000..5f0f8a3f2a9f79 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/prj.conf @@ -0,0 +1,21 @@ +CONFIG_ZTEST=y + +CONFIG_BT=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_SMP=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_GATT_AUTO_DISCOVER_CCC=y +CONFIG_BT_AUDIO=y +CONFIG_BT_CCP_CALL_CONTROL_CLIENT=y +CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT=2 +CONFIG_BT_TBS_CLIENT_TBS=y +CONFIG_BT_TBS_CLIENT_GTBS=y +CONFIG_UTF8=y + +CONFIG_ASSERT=y +CONFIG_ASSERT_LEVEL=2 +CONFIG_ASSERT_VERBOSE=y + +CONFIG_LOG=y +CONFIG_BT_CCP_CALL_CONTROL_CLIENT_LOG_LEVEL_DBG=y diff --git a/tests/bluetooth/audio/ccp_call_control_client/src/main.c b/tests/bluetooth/audio/ccp_call_control_client/src/main.c new file mode 100644 index 00000000000000..34b569d9359343 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/src/main.c @@ -0,0 +1,212 @@ +/* main.c - Application main entry point */ + +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "conn.h" +#include "ccp_call_control_client.h" +#include "expects_util.h" + +DEFINE_FFF_GLOBALS; + +struct ccp_call_control_client_test_suite_fixture { + /** Need 1 additional bearer than the max to trigger some corner cases */ + struct bt_ccp_call_control_client_bearer + *bearers[CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT + 1]; + struct bt_ccp_call_control_client *client; + struct bt_conn conn; +}; + +static void mock_init_rule_before(const struct ztest_unit_test *test, void *fixture) +{ + mock_ccp_call_control_client_init(); +} + +static void mock_destroy_rule_after(const struct ztest_unit_test *test, void *fixture) +{ + mock_ccp_call_control_client_cleanup(); +} + +ZTEST_RULE(mock_rule, mock_init_rule_before, mock_destroy_rule_after); + +static void test_conn_init(struct bt_conn *conn) +{ + conn->index = 0; + conn->info.type = BT_CONN_TYPE_LE; + conn->info.role = BT_CONN_ROLE_CENTRAL; + conn->info.state = BT_CONN_STATE_CONNECTED; + conn->info.security.level = BT_SECURITY_L2; + conn->info.security.enc_key_size = BT_ENC_KEY_SIZE_MAX; + conn->info.security.flags = BT_SECURITY_FLAG_OOB | BT_SECURITY_FLAG_SC; + + mock_bt_conn_connected(conn, BT_HCI_ERR_SUCCESS); +} + +static void *ccp_call_control_client_test_suite_setup(void) +{ + struct ccp_call_control_client_test_suite_fixture *fixture; + + fixture = malloc(sizeof(*fixture)); + zassert_not_null(fixture); + + return fixture; +} + +static void ccp_call_control_client_test_suite_before(void *f) +{ + struct ccp_call_control_client_test_suite_fixture *fixture = f; + + memset(fixture, 0, sizeof(*fixture)); + test_conn_init(&fixture->conn); +} + +static void ccp_call_control_client_test_suite_after(void *f) +{ + struct ccp_call_control_client_test_suite_fixture *fixture = f; + + (void)bt_ccp_call_control_client_unregister_cb(&mock_ccp_call_control_client_cb); + mock_bt_conn_disconnected(&fixture->conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); +} + +static void ccp_call_control_client_test_suite_teardown(void *f) +{ + free(f); +} + +ZTEST_SUITE(ccp_call_control_client_test_suite, NULL, ccp_call_control_client_test_suite_setup, + ccp_call_control_client_test_suite_before, ccp_call_control_client_test_suite_after, + ccp_call_control_client_test_suite_teardown); + +static ZTEST_F(ccp_call_control_client_test_suite, test_ccp_call_control_client_register_cb) +{ + int err; + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_test_suite, + test_ccp_call_control_client_register_cb_inval_param_null) +{ + int err; + + err = bt_ccp_call_control_client_register_cb(NULL); + zassert_equal(-EINVAL, err, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_test_suite, + test_ccp_call_control_client_register_cb_inval_double_register) +{ + int err; + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(-EEXIST, err, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_test_suite, test_ccp_call_control_client_unregister_cb) +{ + int err; + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_unregister_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_test_suite, + test_ccp_call_control_client_unregister_cb_inval_param_null) +{ + int err; + + err = bt_ccp_call_control_client_unregister_cb(NULL); + zassert_equal(-EINVAL, err, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_test_suite, + test_ccp_call_control_client_unregister_cb_inval_double_unregister) +{ + int err; + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_unregister_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_unregister_cb(&mock_ccp_call_control_client_cb); + zassert_equal(-EALREADY, err, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_test_suite, test_ccp_call_control_client_discover) +{ + int err; + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_discover(&fixture->conn, &fixture->client); + zassert_equal(0, err, "Unexpected return value %d", err); + + /* Validate that we got the callback with valid values */ + zexpect_call_count("bt_ccp_call_control_client_cb.discover", 1, + mock_ccp_call_control_client_discover_cb_fake.call_count); + zassert_not_null(mock_ccp_call_control_client_discover_cb_fake.arg0_history[0]); + zassert_equal(0, mock_ccp_call_control_client_discover_cb_fake.arg1_history[0]); + zassert_not_null(mock_ccp_call_control_client_discover_cb_fake.arg2_history[0]); + +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) + zassert_not_null( + mock_ccp_call_control_client_discover_cb_fake.arg2_history[0]->gtbs_bearer); +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ + +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + zassert_equal(CONFIG_BT_TBS_CLIENT_MAX_TBS_INSTANCES, + mock_ccp_call_control_client_discover_cb_fake.arg2_history[0]->tbs_count); + zassert_not_null( + mock_ccp_call_control_client_discover_cb_fake.arg2_history[0]->tbs_bearers); +#endif /* CONFIG_BT_TBS_CLIENT_TBS */ +} + +static ZTEST_F(ccp_call_control_client_test_suite, + test_ccp_call_control_client_discover_inval_param_null_conn) +{ + int err; + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_discover(NULL, &fixture->client); + zassert_equal(-EINVAL, err, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_test_suite, + test_ccp_call_control_client_discover_inval_param_null_client) +{ + int err; + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_discover(&fixture->conn, NULL); + zassert_equal(-EINVAL, err, "Unexpected return value %d", err); +} diff --git a/tests/bluetooth/audio/ccp_call_control_client/testcase.yaml b/tests/bluetooth/audio/ccp_call_control_client/testcase.yaml new file mode 100644 index 00000000000000..007a4f45ef5772 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/testcase.yaml @@ -0,0 +1,7 @@ +common: + tags: + - bluetooth + - bluetooth_audio +tests: + bluetooth.audio.ccp_call_control_client.test: + type: unit diff --git a/tests/bluetooth/audio/ccp_call_control_client/uut/CMakeLists.txt b/tests/bluetooth/audio/ccp_call_control_client/uut/CMakeLists.txt new file mode 100644 index 00000000000000..1347cc8e0f1958 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/uut/CMakeLists.txt @@ -0,0 +1,22 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# +# CMakeLists.txt file for creating of uut library. +# + +add_library(uut STATIC + ${ZEPHYR_BASE}/lib/net_buf/buf_simple.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/ccp_call_control_client.c + ${ZEPHYR_BASE}/subsys/logging/log_minimal.c + ccp_call_control_client.c + tbs_client.c +) + +add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/audio/mocks mocks) + +target_link_libraries(uut PUBLIC test_interface mocks) +target_include_directories(uut PRIVATE ${ZEPHYR_BASE}/tests/bluetooth/audio/ccp_call_control_client/include) + +target_compile_options(uut PRIVATE -std=c11 -include ztest.h) diff --git a/tests/bluetooth/audio/ccp_call_control_client/uut/ccp_call_control_client.c b/tests/bluetooth/audio/ccp_call_control_client/uut/ccp_call_control_client.c new file mode 100644 index 00000000000000..5b4e2363217226 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/uut/ccp_call_control_client.c @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +#include +#include +#include + +#include "ccp_call_control_client.h" + +/* List of fakes used by this unit tester */ +#define FFF_FAKES_LIST(FAKE) FAKE(mock_ccp_call_control_client_discover_cb) + +DEFINE_FAKE_VOID_FUNC(mock_ccp_call_control_client_discover_cb, struct bt_ccp_call_control_client *, + int, struct bt_ccp_call_control_client_bearers *); + +struct bt_ccp_call_control_client_cb mock_ccp_call_control_client_cb = { + .discover = mock_ccp_call_control_client_discover_cb, +}; + +void mock_ccp_call_control_client_init(void) +{ + FFF_FAKES_LIST(RESET_FAKE); +} + +void mock_ccp_call_control_client_cleanup(void) +{ +} diff --git a/tests/bluetooth/audio/ccp_call_control_client/uut/tbs_client.c b/tests/bluetooth/audio/ccp_call_control_client/uut/tbs_client.c new file mode 100644 index 00000000000000..87564da4587581 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/uut/tbs_client.c @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include +#include +#include + +static struct bt_tbs_client_cb *tbs_cbs; + +int bt_tbs_client_register_cb(struct bt_tbs_client_cb *cbs) +{ + tbs_cbs = cbs; + + return 0; +} + +int bt_tbs_client_discover(struct bt_conn *conn) +{ + if (tbs_cbs != NULL && tbs_cbs->discover != NULL) { + uint8_t tbs_cnt = 0; + + IF_ENABLED(CONFIG_BT_TBS_CLIENT_TBS, + (tbs_cnt += CONFIG_BT_TBS_CLIENT_MAX_TBS_INSTANCES)); + + tbs_cbs->discover(conn, 0, tbs_cnt, IS_ENABLED(CONFIG_BT_TBS_CLIENT_GTBS)); + } + + return 0; +} diff --git a/tests/bluetooth/audio/mocks/include/conn.h b/tests/bluetooth/audio/mocks/include/conn.h index ae6d3dc70d4690..bcf9ca5c8ae811 100644 --- a/tests/bluetooth/audio/mocks/include/conn.h +++ b/tests/bluetooth/audio/mocks/include/conn.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2023 Codecoup + * Copyright (c) 2024 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -7,6 +8,8 @@ #ifndef MOCKS_CONN_H_ #define MOCKS_CONN_H_ +#include + #include struct bt_conn { @@ -15,6 +18,7 @@ struct bt_conn { struct bt_iso_chan *chan; }; +void mock_bt_conn_connected(struct bt_conn *conn, uint8_t err); void mock_bt_conn_disconnected(struct bt_conn *conn, uint8_t err); #endif /* MOCKS_CONN_H_ */ diff --git a/tests/bluetooth/audio/mocks/include/expects_util.h b/tests/bluetooth/audio/mocks/include/expects_util.h index 7cb630b7b33d11..55462dabd6c3a5 100644 --- a/tests/bluetooth/audio/mocks/include/expects_util.h +++ b/tests/bluetooth/audio/mocks/include/expects_util.h @@ -7,9 +7,13 @@ #ifndef MOCKS_UTIL_H_ #define MOCKS_UTIL_H_ +#include + #include #include +#include #include +#include #define CHECK_EMPTY(_x) UTIL_BOOL(IS_EMPTY(_x)) #define COND_CODE_EMPTY(_x, _if_any_code, _else_code) \ diff --git a/tests/bluetooth/audio/mocks/src/conn.c b/tests/bluetooth/audio/mocks/src/conn.c index d7570bd0f5f682..0fd9e698888070 100644 --- a/tests/bluetooth/audio/mocks/src/conn.c +++ b/tests/bluetooth/audio/mocks/src/conn.c @@ -1,10 +1,14 @@ /* * Copyright (c) 2023 Codecoup + * Copyright (c) 2024 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ +#include + #include +#include #include "conn.h" @@ -27,7 +31,15 @@ struct bt_conn *bt_conn_ref(struct bt_conn *conn) void bt_conn_unref(struct bt_conn *conn) { +} +void mock_bt_conn_connected(struct bt_conn *conn, uint8_t err) +{ + STRUCT_SECTION_FOREACH(bt_conn_cb, cb) { + if (cb->connected) { + cb->connected(conn, err); + } + } } void mock_bt_conn_disconnected(struct bt_conn *conn, uint8_t err) diff --git a/tests/bluetooth/shell/audio.conf b/tests/bluetooth/shell/audio.conf index 7b5a67d5b865fa..cf9d040860fccb 100644 --- a/tests/bluetooth/shell/audio.conf +++ b/tests/bluetooth/shell/audio.conf @@ -136,6 +136,8 @@ CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT=2 CONFIG_BT_TBS=y CONFIG_BT_TBS_BEARER_COUNT=1 CONFIG_BT_TBS_SUPPORTED_FEATURES=3 +CONFIG_BT_CCP_CALL_CONTROL_CLIENT=y +CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT=2 CONFIG_BT_TBS_CLIENT_TBS=y CONFIG_BT_TBS_CLIENT_GTBS=y diff --git a/tests/bluetooth/shell/testcase.yaml b/tests/bluetooth/shell/testcase.yaml index 6de8c815dc5326..e626262f5f55f7 100644 --- a/tests/bluetooth/shell/testcase.yaml +++ b/tests/bluetooth/shell/testcase.yaml @@ -327,6 +327,7 @@ tests: extra_configs: - CONFIG_BT_TBS_CLIENT_TBS=n - CONFIG_BT_TBS_CLIENT_GTBS=n + - CONFIG_BT_CCP_CALL_CONTROL_CLIENT=n tags: bluetooth bluetooth.shell.audio.tbs_only_client: extra_args: CONF_FILE="audio.conf" @@ -339,6 +340,7 @@ tests: build_only: true extra_configs: - CONFIG_BT_TBS_CLIENT_TBS=n + - CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT=1 tags: bluetooth bluetooth.audio_shell.no_cap_acceptor: extra_args: CONF_FILE="audio.conf" diff --git a/tests/bsim/bluetooth/audio/prj.conf b/tests/bsim/bluetooth/audio/prj.conf index 3cc57db36e94b0..2fd01fb4cfc34e 100644 --- a/tests/bsim/bluetooth/audio/prj.conf +++ b/tests/bsim/bluetooth/audio/prj.conf @@ -93,6 +93,8 @@ CONFIG_BT_CSIP_SET_COORDINATOR_TEST_SAMPLE_DATA=y CONFIG_BT_CCP_CALL_CONTROL_SERVER=y CONFIG_BT_TBS=y CONFIG_BT_TBS_BEARER_COUNT=1 + +CONFIG_BT_CCP_CALL_CONTROL_CLIENT=y CONFIG_BT_TBS_CLIENT_TBS=y CONFIG_BT_TBS_CLIENT_GTBS=y CONFIG_BT_TBS_CLIENT_MAX_CALLS=4 diff --git a/tests/bsim/bluetooth/audio/src/ccp_call_control_client_test.c b/tests/bsim/bluetooth/audio/src/ccp_call_control_client_test.c new file mode 100644 index 00000000000000..6a7b8019a6037e --- /dev/null +++ b/tests/bsim/bluetooth/audio/src/ccp_call_control_client_test.c @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "bstests.h" +#include "common.h" + +#ifdef CONFIG_BT_CCP_CALL_CONTROL_CLIENT +LOG_MODULE_REGISTER(ccp_call_control_client, CONFIG_LOG_DEFAULT_LEVEL); + +extern enum bst_result_t bst_result; + +CREATE_FLAG(flag_discovery_complete); + +static struct bt_ccp_call_control_client *inst; + +static void ccp_call_control_client_discover_cb(struct bt_ccp_call_control_client *client, int err, + struct bt_ccp_call_control_client_bearers *bearers) +{ + if (err != 0) { + FAIL("Failed to discover TBS: %d\n", err); + return; + } + + LOG_INF("Discovery completed with %s%u TBS bearers", + bearers->gtbs_bearer != NULL ? "GTBS and " : "", bearers->tbs_count); + + if (IS_ENABLED(CONFIG_BT_TBS_CLIENT_TBS) && bearers->gtbs_bearer == NULL) { + FAIL("Failed to discover GTBS"); + return; + } + + SET_FLAG(flag_discovery_complete); +} + +static void discover_tbs(void) +{ + int err; + + UNSET_FLAG(flag_discovery_complete); + + err = bt_ccp_call_control_client_discover(default_conn, &inst); + if (err) { + FAIL("Failed to discover TBS: %d", err); + return; + } + + WAIT_FOR_FLAG(flag_discovery_complete); +} + +static void init(void) +{ + static struct bt_ccp_call_control_client_cb ccp_call_control_client_cbs = { + .discover = ccp_call_control_client_discover_cb, + }; + int err; + + err = bt_enable(NULL); + if (err != 0) { + FAIL("Bluetooth discover failed (err %d)\n", err); + return; + } + + err = bt_ccp_call_control_client_register_cb(&ccp_call_control_client_cbs); + if (err != 0) { + FAIL("Failed to register CCP Call Control Client cbs (err %d)\n", err); + return; + } +} + +static void test_main(void) +{ + int err; + + init(); + + err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_2, ad, AD_SIZE, NULL, 0); + if (err != 0) { + FAIL("Advertising failed to start (err %d)\n", err); + return; + } + + LOG_INF("Advertising successfully started\n"); + + WAIT_FOR_FLAG(flag_connected); + + discover_tbs(); + discover_tbs(); /* test that we can discover twice */ + + err = bt_conn_disconnect(default_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err != 0) { + FAIL("Failed to disconnect: %d\n", err); + } + + WAIT_FOR_FLAG(flag_disconnected); + + PASS("CCP Call Control Client Passed\n"); +} + +static const struct bst_test_instance test_ccp_call_control_client[] = { + { + .test_id = "ccp_call_control_client", + .test_pre_init_f = test_init, + .test_tick_f = test_tick, + .test_main_f = test_main, + }, + BSTEST_END_MARKER, +}; + +struct bst_test_list *test_ccp_call_control_client_install(struct bst_test_list *tests) +{ + return bst_add_tests(tests, test_ccp_call_control_client); +} + +#else +struct bst_test_list *test_ccp_call_control_client_install(struct bst_test_list *tests) +{ + return tests; +} + +#endif /* CONFIG_BT_CCP_CALL_CONTROL_CLIENT */ diff --git a/tests/bsim/bluetooth/audio/src/ccp_call_control_server_test.c b/tests/bsim/bluetooth/audio/src/ccp_call_control_server_test.c index dfc6ef579531ee..ab8771f09276da 100644 --- a/tests/bsim/bluetooth/audio/src/ccp_call_control_server_test.c +++ b/tests/bsim/bluetooth/audio/src/ccp_call_control_server_test.c @@ -118,6 +118,12 @@ static void init(void) LOG_INF("Registered bearer[%d]", i); } + + err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, NULL); + if (err != 0) { + FAIL("Scanning failed to start (err %d)\n", err); + return; + } } static void unregister_bearers(void) @@ -143,6 +149,10 @@ static void test_main(void) { init(); + WAIT_FOR_FLAG(flag_connected); + + WAIT_FOR_FLAG(flag_disconnected); + unregister_bearers(); PASS("CCP Call Control Server Passed\n"); diff --git a/tests/bsim/bluetooth/audio/src/main.c b/tests/bsim/bluetooth/audio/src/main.c index 7fc13176bae9c1..3e8161fe7de49a 100644 --- a/tests/bsim/bluetooth/audio/src/main.c +++ b/tests/bsim/bluetooth/audio/src/main.c @@ -44,6 +44,7 @@ extern struct bst_test_list *test_csip_notify_client_install(struct bst_test_lis extern struct bst_test_list *test_csip_notify_server_install(struct bst_test_list *tests); extern struct bst_test_list *test_gmap_ugg_install(struct bst_test_list *tests); extern struct bst_test_list *test_gmap_ugt_install(struct bst_test_list *tests); +extern struct bst_test_list *test_ccp_call_control_client_install(struct bst_test_list *tests); extern struct bst_test_list *test_ccp_call_control_server_install(struct bst_test_list *tests); bst_test_install_t test_installers[] = { @@ -83,6 +84,7 @@ bst_test_install_t test_installers[] = { test_csip_notify_server_install, test_gmap_ugg_install, test_gmap_ugt_install, + test_ccp_call_control_client_install, test_ccp_call_control_server_install, NULL, }; diff --git a/tests/bsim/bluetooth/audio/test_scripts/ccp.sh b/tests/bsim/bluetooth/audio/test_scripts/ccp.sh index cac742afbd84c8..8370f27d751ce6 100755 --- a/tests/bsim/bluetooth/audio/test_scripts/ccp.sh +++ b/tests/bsim/bluetooth/audio/test_scripts/ccp.sh @@ -13,10 +13,12 @@ cd ${BSIM_OUT_PATH}/bin SIMULATION_ID="ccp" Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_prj_conf \ - -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} -d=0 -testid=ccp_call_control_server -rs=1 -D=1 + -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} -d=0 -testid=ccp_call_control_server -rs=1 -D=2 + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_prj_conf \ + -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} -d=1 -testid=ccp_call_control_client -rs=2 -D=2 # Simulation time should be larger than the WAIT_TIME in common.h -Execute ./bs_2G4_phy_v1 -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} \ - -D=1 -sim_length=60e6 $@ +Execute ./bs_2G4_phy_v1 -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} -D=2 -sim_length=60e6 $@ wait_for_background_jobs diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/CMakeLists.txt b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/CMakeLists.txt new file mode 100644 index 00000000000000..da0ca3feddbaac --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/CMakeLists.txt @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ccp_call_control_client_self_tets) + +set(ccp_call_control_client_path ${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_client) + +target_sources(app PRIVATE + ${ccp_call_control_client_path}/src/main.c +) + +target_sources(app PRIVATE + src/test_main.c +) + +zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ + ) diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/Kconfig.sysbuild b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/Kconfig.sysbuild new file mode 100644 index 00000000000000..2fdcdcf687c67f --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/Kconfig.sysbuild @@ -0,0 +1,10 @@ +# Copyright (c) 2023-2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_client/Kconfig.sysbuild" + +config NATIVE_SIMULATOR_PRIMARY_MCU_INDEX + int + # Let's pass the test arguments to the application MCU test + # otherwise by default they would have gone to the net core. + default 0 if $(BOARD_TARGET_STRING) = "NRF5340BSIM_NRF5340_CPUAPP" diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/prj.conf b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/prj.conf new file mode 100644 index 00000000000000..b89931736c1dcd --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/prj.conf @@ -0,0 +1,2 @@ +# Please build using the sample configuration file: +# ${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_client/prj.conf diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/src/test_main.c b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/src/test_main.c new file mode 100644 index 00000000000000..84c550b878bced --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/src/test_main.c @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "bs_types.h" +#include "bs_tracing.h" +#include "bs_utils.h" +#include "bstests.h" + +#define WAIT_TIME 10 /* Seconds */ + +extern enum bst_result_t bst_result; + +#define FAIL(...) \ + do { \ + bst_result = Failed; \ + bs_trace_error_time_line(__VA_ARGS__); \ + } while (0) + +#define PASS(...) \ + do { \ + bst_result = Passed; \ + bs_trace_info_time(1, __VA_ARGS__); \ + } while (0) + +static void test_ccp_call_control_client_sample_init(void) +{ + bst_ticker_set_next_tick_absolute(WAIT_TIME * 1e6); + bst_result = In_progress; +} + +static void test_ccp_call_control_client_sample_tick(bs_time_t HW_device_time) +{ + extern struct bt_ccp_call_control_client *client; + + /* If discovery was a success then client is non-NULL - Use as pass criteria */ + if (client == NULL) { + FAIL("CCP Call Control Client sample FAILED (Did not pass after %i seconds)\n", + WAIT_TIME); + } else { + PASS("CCP Call Control Client sample PASSED\n"); + } +} + +static const struct bst_test_instance test_sample[] = { + { + .test_id = "ccp_call_control_client", + .test_descr = "Test based on the CCP Call Control Client sample. " + "It expects to be connected to a compatible CCP server, " + "waits for " STR(WAIT_TIME) " seconds, and checks how " + "many audio packets have been received correctly", + .test_post_init_f = test_ccp_call_control_client_sample_init, + .test_tick_f = test_ccp_call_control_client_sample_tick, + }, + BSTEST_END_MARKER, +}; + +static struct bst_test_list * +test_ccp_call_control_client_sample_install(struct bst_test_list *tests) +{ + tests = bst_add_tests(tests, test_sample); + return tests; +} + +bst_test_install_t test_installers[] = {test_ccp_call_control_client_sample_install, NULL}; diff --git a/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/sysbuild.cmake b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/sysbuild.cmake new file mode 100644 index 00000000000000..3af3a8a3824024 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/ccp/call_control_client/sysbuild.cmake @@ -0,0 +1,6 @@ +# Copyright (c) 2023-2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +include(${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_client/sysbuild.cmake) + +native_simulator_set_primary_mcu_index(${DEFAULT_IMAGE} ${NET_APP}) diff --git a/tests/bsim/bluetooth/audio_samples/ccp/compile.sh b/tests/bsim/bluetooth/audio_samples/ccp/compile.sh index 186805e7ec4169..cfc92e66c788ee 100755 --- a/tests/bsim/bluetooth/audio_samples/ccp/compile.sh +++ b/tests/bsim/bluetooth/audio_samples/ccp/compile.sh @@ -15,12 +15,22 @@ if [ "${BOARD_TS}" == "nrf5340bsim_nrf5340_cpuapp" ]; then conf_file=${sample}/prj.conf \ conf_overlay=${sample}/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf \ exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile + app=tests/bsim/bluetooth/audio_samples/ccp/call_control_client \ + sample=${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_client \ + conf_file=${sample}/prj.conf \ + conf_overlay=${sample}/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf \ + exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile else app=tests/bsim/bluetooth/audio_samples/ccp/call_control_server \ sample=${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_server \ conf_file=${sample}/prj.conf \ conf_overlay=${sample}/overlay-bt_ll_sw_split.conf \ exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile + app=tests/bsim/bluetooth/audio_samples/ccp/call_control_client \ + sample=${ZEPHYR_BASE}/samples/bluetooth/ccp_call_control_client \ + conf_file=${sample}/prj.conf \ + conf_overlay=${sample}/overlay-bt_ll_sw_split.conf \ + exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile fi wait_for_background_jobs diff --git a/tests/bsim/bluetooth/audio_samples/ccp/tests_scripts/ccp.sh b/tests/bsim/bluetooth/audio_samples/ccp/tests_scripts/ccp.sh index 87564607df7ab1..e366ea54eee11b 100755 --- a/tests/bsim/bluetooth/audio_samples/ccp/tests_scripts/ccp.sh +++ b/tests/bsim/bluetooth/audio_samples/ccp/tests_scripts/ccp.sh @@ -14,7 +14,10 @@ cd ${BSIM_OUT_PATH}/bin Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_samples_ccp_call_control_server_prj_conf \ -v=${verbosity_level} -s=${simulation_id} -d=0 -RealEncryption=1 -testid=ccp_call_control_server +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_samples_ccp_call_control_client_prj_conf \ + -v=${verbosity_level} -s=${simulation_id} -d=1 -RealEncryption=1 -testid=ccp_call_control_client + Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} \ - -D=1 -sim_length=20e6 $@ -argschannel -at=40 + -D=2 -sim_length=20e6 $@ -argschannel -at=40 wait_for_background_jobs #Wait for all programs in background and return != 0 if any fails From 605ccf5a23b74d0a9bbaa0c01755d07363fee48b Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Wed, 2 Oct 2024 10:12:19 +0200 Subject: [PATCH 3/5] Bluetooth: CCP: Add support for set/get provider name Add support for setting and getting the bearer provider name. For now the name will be duplicated by the TBS implementation, but will be optimizied in the future so only one copy of the name exists. Signed-off-by: Emil Gydesen --- .../bluetooth/shell/audio/ccp.rst | 27 +++- include/zephyr/bluetooth/audio/ccp.h | 27 ++++ subsys/bluetooth/audio/Kconfig.ccp | 7 + subsys/bluetooth/audio/Kconfig.tbs | 2 +- .../bluetooth/audio/ccp_call_control_server.c | 69 ++++++++ .../audio/shell/ccp_call_control_server.c | 81 ++++++++++ subsys/bluetooth/audio/tbs.c | 4 + .../audio/ccp_call_control_server/src/main.c | 149 +++++++++++++++++- 8 files changed, 363 insertions(+), 3 deletions(-) diff --git a/doc/connectivity/bluetooth/shell/audio/ccp.rst b/doc/connectivity/bluetooth/shell/audio/ccp.rst index d25ea71d606f12..f5c3be3310a0de 100644 --- a/doc/connectivity/bluetooth/shell/audio/ccp.rst +++ b/doc/connectivity/bluetooth/shell/audio/ccp.rst @@ -13,12 +13,18 @@ The Server can be controlled locally, or by a remote device (when in a call). Fo example a remote device may initiate a call to the server, or the Server may initiate a call to remote device, without a client. +For all commands that take an optional :code:`index`, if the index is not supplied then it defaults +to :code:`0` which is the GTBS bearer. + .. code-block:: console ccp_call_control_server --help ccp_call_control_server - Bluetooth CCP Call Control Server shell commands Subcommands: - init : Initialize CCP Call Control Server + init : Initialize CCP Call Control Server + set_bearer_name : Set bearer name [index] + get_bearer_name : Get bearer name [index] + Example Usage ============= @@ -34,6 +40,25 @@ Setup Registered bearer[1] uart:~$ bt connect xx:xx:xx:xx:xx:xx public +Setting and getting the bearer name +----------------------------------- + +.. code-block:: console + + uart:~$ ccp_call_control_server get_bearer_name + Bearer[0] name: Generic TBS + uart:~$ ccp_call_control_server set_bearer_name "New name" + Bearer[0] name: New name + uart:~$ ccp_call_control_server get_bearer_name + Bearer[0] name: New name + uart:~$ ccp_call_control_server get_bearer_name 1 + Bearer[1] name: Telephone Bearer #1 + uart:~$ ccp_call_control_server set_bearer_name 1 "New TBS name" + Bearer[1] name: New TBS name + uart:~$ ccp_call_control_server get_bearer_name 1 + Bearer[1] name: New TBS name + + Call Control Client ******************* The Call Control Client is a role that typically resides on resource contrained devices such as diff --git a/include/zephyr/bluetooth/audio/ccp.h b/include/zephyr/bluetooth/audio/ccp.h index 190d58313f272a..a0288d5c51ee3e 100644 --- a/include/zephyr/bluetooth/audio/ccp.h +++ b/include/zephyr/bluetooth/audio/ccp.h @@ -92,6 +92,33 @@ int bt_ccp_call_control_server_register_bearer(const struct bt_tbs_register_para */ int bt_ccp_call_control_server_unregister_bearer(struct bt_ccp_call_control_server_bearer *bearer); +/** + * @brief Set a new bearer provider name. + * + * @param bearer The bearer to set the name for. + * @param name The new bearer provider name. + * + * @retval 0 Success + * @retval -EINVAL @p bearer or @p name is NULL, or @p name is the empty string or @p name is larger + * than @kconfig{CONFIG_BT_TBS_MAX_PROVIDER_NAME_LENGTH} + * @retval -EFAULT @p bearer is not registered + */ +int bt_ccp_call_control_server_set_bearer_provider_name( + struct bt_ccp_call_control_server_bearer *bearer, const char *name); + +/** + * @brief Get the bearer provider name. + * + * @param[in] bearer The bearer to get the name for. + * @param[out] name Pointer that will be updated to be the bearer provider name. + * + * @retval 0 Success + * @retval -EINVAL @p bearer or @p name is NULL + * @retval -EFAULT @p bearer is not registered + */ +int bt_ccp_call_control_server_get_bearer_provider_name( + struct bt_ccp_call_control_server_bearer *bearer, const char **name); + /** @} */ /* End of group bt_ccp_call_control_server */ /** diff --git a/subsys/bluetooth/audio/Kconfig.ccp b/subsys/bluetooth/audio/Kconfig.ccp index c397b831cfc01b..c99b76bc664da8 100644 --- a/subsys/bluetooth/audio/Kconfig.ccp +++ b/subsys/bluetooth/audio/Kconfig.ccp @@ -50,6 +50,13 @@ config BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT help The number of supported telephone bearers on the CCP Call Control Server +config BT_ccp_call_control_server_PROVIDER_NAME_MAX_LENGTH + int "The maximum length of the bearer provider name excluding null terminator" + default BT_TBS_MAX_PROVIDER_NAME_LENGTH + range 1 BT_TBS_MAX_PROVIDER_NAME_LENGTH + help + Sets the maximum length of the bearer provider name. + module = BT_CCP_CALL_CONTROL_SERVER module-str = "Call Control Profile Call Control Server" source "subsys/logging/Kconfig.template.log_config" diff --git a/subsys/bluetooth/audio/Kconfig.tbs b/subsys/bluetooth/audio/Kconfig.tbs index 14729499bee461..e3565fe280da85 100644 --- a/subsys/bluetooth/audio/Kconfig.tbs +++ b/subsys/bluetooth/audio/Kconfig.tbs @@ -250,7 +250,7 @@ config BT_TBS_MAX_URI_LENGTH config BT_TBS_MAX_PROVIDER_NAME_LENGTH int "The maximum length of the bearer provider name" default 30 - range 0 512 + range 1 512 help Sets the maximum length of the bearer provider name. diff --git a/subsys/bluetooth/audio/ccp_call_control_server.c b/subsys/bluetooth/audio/ccp_call_control_server.c index a51354f4ce42dc..936770fa4bda13 100644 --- a/subsys/bluetooth/audio/ccp_call_control_server.c +++ b/subsys/bluetooth/audio/ccp_call_control_server.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ LOG_MODULE_REGISTER(bt_ccp_call_control_server, CONFIG_BT_CCP_CALL_CONTROL_SERVE /* A service instance can either be a GTBS or a TBS instance */ struct bt_ccp_call_control_server_bearer { + char provider_name[CONFIG_BT_ccp_call_control_server_PROVIDER_NAME_MAX_LENGTH + 1]; uint8_t tbs_index; bool registered; }; @@ -69,6 +71,8 @@ int bt_ccp_call_control_server_register_bearer(const struct bt_tbs_register_para free_bearer->registered = true; free_bearer->tbs_index = (uint8_t)ret; + (void)utf8_lcpy(free_bearer->provider_name, param->provider_name, + sizeof(free_bearer->provider_name)); *bearer = free_bearer; return 0; @@ -104,3 +108,68 @@ int bt_ccp_call_control_server_unregister_bearer(struct bt_ccp_call_control_serv return 0; } + +int bt_ccp_call_control_server_set_bearer_provider_name( + struct bt_ccp_call_control_server_bearer *bearer, const char *name) +{ + size_t len; + + CHECKIF(bearer == NULL) { + LOG_DBG("bearer is NULL"); + + return -EINVAL; + } + + CHECKIF(name == NULL) { + LOG_DBG("name is NULL"); + + return -EINVAL; + } + + if (!bearer->registered) { + LOG_DBG("Bearer %p not registered", bearer); + + return -EFAULT; + } + + len = strlen(name); + if (len > CONFIG_BT_ccp_call_control_server_PROVIDER_NAME_MAX_LENGTH || len == 0) { + LOG_DBG("Invalid name length: %zu", len); + + return -EINVAL; + } + + if (strcmp(bearer->provider_name, name) == 0) { + return 0; + } + + (void)utf8_lcpy(bearer->provider_name, name, sizeof(bearer->provider_name)); + + return bt_tbs_set_bearer_provider_name(bearer->tbs_index, name); +} + +int bt_ccp_call_control_server_get_bearer_provider_name( + struct bt_ccp_call_control_server_bearer *bearer, const char **name) +{ + CHECKIF(bearer == NULL) { + LOG_DBG("bearer is NULL"); + + return -EINVAL; + } + + CHECKIF(name == NULL) { + LOG_DBG("name is NULL"); + + return -EINVAL; + } + + if (!bearer->registered) { + LOG_DBG("Bearer %p not registered", bearer); + + return -EFAULT; + } + + *name = bearer->provider_name; + + return 0; +} diff --git a/subsys/bluetooth/audio/shell/ccp_call_control_server.c b/subsys/bluetooth/audio/shell/ccp_call_control_server.c index 0d76cc5877a0a1..7150bf8af72690 100644 --- a/subsys/bluetooth/audio/shell/ccp_call_control_server.c +++ b/subsys/bluetooth/audio/shell/ccp_call_control_server.c @@ -15,6 +15,7 @@ #include #include #include +#include static struct bt_ccp_call_control_server_bearer *bearers[CONFIG_BT_CCP_CALL_CONTROL_SERVER_BEARER_COUNT]; @@ -79,6 +80,81 @@ static int cmd_ccp_call_control_server_init(const struct shell *sh, size_t argc, return 0; } +static int validate_and_get_index(const struct shell *sh, const char *index_arg) +{ + unsigned long index; + int err = 0; + + index = shell_strtoul(index_arg, 0, &err); + if (err != 0) { + shell_error(sh, "Could not parse index: %d", err); + + return -ENOEXEC; + } + + if (index > CONFIG_BT_TBS_BEARER_COUNT) { + shell_error(sh, "Invalid index: %lu", index); + + return -ENOEXEC; + } + + return (int)index; +} + +static int cmd_ccp_call_control_server_set_bearer_name(const struct shell *sh, size_t argc, + char *argv[]) +{ + unsigned long index = 0; + const char *name; + int err = 0; + + if (argc > 2) { + index = validate_and_get_index(sh, argv[1]); + if (index < 0) { + return index; + } + } + + name = argv[argc - 1]; + + err = bt_ccp_call_control_server_set_bearer_provider_name(bearers[index], name); + if (err != 0) { + shell_error(sh, "Failed to set bearer[%lu] name: %d", index, err); + + return -ENOEXEC; + } + + shell_print(sh, "Bearer[%lu] name: %s", index, name); + + return 0; +} + +static int cmd_ccp_call_control_server_get_bearer_name(const struct shell *sh, size_t argc, + char *argv[]) +{ + unsigned long index = 0; + const char *name; + int err = 0; + + if (argc > 1) { + index = validate_and_get_index(sh, argv[1]); + if (index < 0) { + return index; + } + } + + err = bt_ccp_call_control_server_get_bearer_provider_name(bearers[index], &name); + if (err != 0) { + shell_error(sh, "Failed to get bearer[%lu] name: %d", index, err); + + return -ENOEXEC; + } + + shell_print(sh, "Bearer[%lu] name: %s", index, name); + + return 0; +} + static int cmd_ccp_call_control_server(const struct shell *sh, size_t argc, char **argv) { if (argc > 1) { @@ -93,6 +169,11 @@ static int cmd_ccp_call_control_server(const struct shell *sh, size_t argc, char SHELL_STATIC_SUBCMD_SET_CREATE(ccp_call_control_server_cmds, SHELL_CMD_ARG(init, NULL, "Initialize CCP Call Control Server", cmd_ccp_call_control_server_init, 1, 0), + SHELL_CMD_ARG(set_bearer_name, NULL, + "Set bearer name [index] ", + cmd_ccp_call_control_server_set_bearer_name, 2, 1), + SHELL_CMD_ARG(get_bearer_name, NULL, "Get bearer name [index]", + cmd_ccp_call_control_server_get_bearer_name, 1, 1), SHELL_SUBCMD_SET_END); SHELL_CMD_ARG_REGISTER(ccp_call_control_server, &ccp_call_control_server_cmds, diff --git a/subsys/bluetooth/audio/tbs.c b/subsys/bluetooth/audio/tbs.c index fb94678375fa25..763580c2b90fc8 100644 --- a/subsys/bluetooth/audio/tbs.c +++ b/subsys/bluetooth/audio/tbs.c @@ -42,6 +42,10 @@ LOG_MODULE_REGISTER(bt_tbs, CONFIG_BT_TBS_LOG_LEVEL); /* A service instance can either be a GTBS or a TBS instance */ struct tbs_inst { /* Attribute values */ + /* TODO: The provider name should be removed from the tbs_inst and instead by stored by the + * user of TBS. This will be done once the CCP API is complete as the CCP Server will own + * all the data instead of the TBS + */ char provider_name[CONFIG_BT_TBS_MAX_PROVIDER_NAME_LENGTH]; char uci[BT_TBS_MAX_UCI_SIZE]; uint8_t technology; diff --git a/tests/bluetooth/audio/ccp_call_control_server/src/main.c b/tests/bluetooth/audio/ccp_call_control_server/src/main.c index 4d45690885f70b..3d5f66f5cee956 100644 --- a/tests/bluetooth/audio/ccp_call_control_server/src/main.c +++ b/tests/bluetooth/audio/ccp_call_control_server/src/main.c @@ -26,6 +26,8 @@ DEFINE_FFF_GLOBALS; +#define DEFAULT_BEARER_NAME "test" + struct ccp_call_control_server_test_suite_fixture { /** Need 1 additional bearer than the max to trigger some corner cases */ struct bt_ccp_call_control_server_bearer @@ -81,7 +83,7 @@ ZTEST_SUITE(ccp_call_control_server_test_suite, NULL, ccp_call_control_server_te static void register_default_bearer(struct ccp_call_control_server_test_suite_fixture *fixture) { const struct bt_tbs_register_param register_param = { - .provider_name = "test", + .provider_name = DEFAULT_BEARER_NAME, .uci = "un999", .uri_schemes_supported = "tel", .gtbs = true, @@ -262,3 +264,148 @@ static ZTEST_F(ccp_call_control_server_test_suite, err = bt_ccp_call_control_server_unregister_bearer(NULL); zassert_equal(err, -EINVAL, "Unexpected return value %d", err); } + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_set_bearer_provider_name) +{ + const char *new_bearer_name = "New bearer name"; + const char *res_bearer_name; + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_set_bearer_provider_name(fixture->bearers[0], + new_bearer_name); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_ccp_call_control_server_get_bearer_provider_name(fixture->bearers[0], + &res_bearer_name); + zassert_equal(err, 0, "Unexpected return value %d", err); + + zassert_str_equal(new_bearer_name, res_bearer_name, "%s != %s", new_bearer_name, + res_bearer_name); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_set_bearer_provider_name_inval_not_registered) +{ + const char *new_bearer_name = "New bearer name"; + int err; + + /* Register and unregister bearer to get a valid pointer but where it is unregistered*/ + register_default_bearer(fixture); + err = bt_ccp_call_control_server_unregister_bearer(fixture->bearers[0]); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_ccp_call_control_server_set_bearer_provider_name(fixture->bearers[0], + new_bearer_name); + zassert_equal(err, -EFAULT, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_set_bearer_provider_name_inval_null_bearer) +{ + const char *new_bearer_name = "New bearer name"; + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_set_bearer_provider_name(NULL, new_bearer_name); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_set_bearer_provider_name_inval_null_name) +{ + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_set_bearer_provider_name(fixture->bearers[0], NULL); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_set_bearer_provider_name_inval_empty_name) +{ + const char *inval_bearer_name = ""; + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_set_bearer_provider_name(fixture->bearers[0], + inval_bearer_name); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_set_bearer_provider_name_inval_long_name) +{ + char inval_bearer_name[CONFIG_BT_ccp_call_control_server_PROVIDER_NAME_MAX_LENGTH + 1]; + int err; + + for (size_t i = 0; i < ARRAY_SIZE(inval_bearer_name); i++) { + inval_bearer_name[i] = 'a'; + } + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_set_bearer_provider_name(fixture->bearers[0], + inval_bearer_name); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_get_bearer_provider_name) +{ + const char *res_bearer_name; + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_get_bearer_provider_name(fixture->bearers[0], + &res_bearer_name); + zassert_equal(err, 0, "Unexpected return value %d", err); + + zassert_str_equal(DEFAULT_BEARER_NAME, res_bearer_name, "%s != %s", DEFAULT_BEARER_NAME, + res_bearer_name); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_get_bearer_provider_name_inval_not_registered) +{ + const char *res_bearer_name; + int err; + + /* Register and unregister bearer to get a valid pointer but where it is unregistered*/ + register_default_bearer(fixture); + err = bt_ccp_call_control_server_unregister_bearer(fixture->bearers[0]); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_ccp_call_control_server_get_bearer_provider_name(fixture->bearers[0], + &res_bearer_name); + zassert_equal(err, -EFAULT, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_get_bearer_provider_name_inval_null_bearer) +{ + const char *res_bearer_name; + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_get_bearer_provider_name(NULL, &res_bearer_name); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_server_test_suite, + test_bt_ccp_call_control_server_get_bearer_provider_name_inval_null_name) +{ + int err; + + register_default_bearer(fixture); + + err = bt_ccp_call_control_server_get_bearer_provider_name(fixture->bearers[0], NULL); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} From 0902ef09d1948146fe3e1785387f68a612ceb725 Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Tue, 8 Oct 2024 12:54:08 +0200 Subject: [PATCH 4/5] Bluetooth: CCP: Client: Add get_bearers Add bt_ccp_client_get_bearers that will return the bearers of a client so that the application can always retrieve them if they do not store them from the discovery callback. Signed-off-by: Emil Gydesen --- include/zephyr/bluetooth/audio/ccp.h | 11 +++++++++ .../bluetooth/audio/ccp_call_control_client.c | 19 +++++++++++++++ .../audio/ccp_call_control_client/src/main.c | 24 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/include/zephyr/bluetooth/audio/ccp.h b/include/zephyr/bluetooth/audio/ccp.h index a0288d5c51ee3e..5188f0a30df5e9 100644 --- a/include/zephyr/bluetooth/audio/ccp.h +++ b/include/zephyr/bluetooth/audio/ccp.h @@ -196,6 +196,17 @@ int bt_ccp_call_control_client_register_cb(struct bt_ccp_call_control_client_cb * @retval -EALREADY @p cb is not registered */ int bt_ccp_call_control_client_unregister_cb(struct bt_ccp_call_control_client_cb *cb); + +/** + * @brief Get the bearers of a client instance + * + * @param[in] client The client to get the bearers of. + * @param[out] bearers The bearers struct that will be populated with the bearers of @p inst. + * @retval -EINVAL @p inst or @p bearers is NULL + */ +int bt_ccp_call_control_client_get_bearers(struct bt_ccp_call_control_client *client, + struct bt_ccp_call_control_client_bearers *bearers); + /** @} */ /* End of group bt_ccp_call_control_client */ #ifdef __cplusplus } diff --git a/subsys/bluetooth/audio/ccp_call_control_client.c b/subsys/bluetooth/audio/ccp_call_control_client.c index 641f887c3f483a..91c582597cb74f 100644 --- a/subsys/bluetooth/audio/ccp_call_control_client.c +++ b/subsys/bluetooth/audio/ccp_call_control_client.c @@ -232,3 +232,22 @@ int bt_ccp_call_control_client_unregister_cb(struct bt_ccp_call_control_client_c return 0; } + +int bt_ccp_call_control_client_get_bearers(struct bt_ccp_call_control_client *client, + struct bt_ccp_call_control_client_bearers *bearers) +{ + CHECKIF(client == NULL) { + LOG_DBG("client is NULL"); + return -EINVAL; + } + + CHECKIF(bearers == NULL) { + LOG_DBG("bearers is NULL"); + return -EINVAL; + } + + memset(bearers, 0, sizeof(*bearers)); + populate_bearers(client, bearers); + + return 0; +} diff --git a/tests/bluetooth/audio/ccp_call_control_client/src/main.c b/tests/bluetooth/audio/ccp_call_control_client/src/main.c index 34b569d9359343..c23d6ed592bb49 100644 --- a/tests/bluetooth/audio/ccp_call_control_client/src/main.c +++ b/tests/bluetooth/audio/ccp_call_control_client/src/main.c @@ -210,3 +210,27 @@ static ZTEST_F(ccp_call_control_client_test_suite, err = bt_ccp_call_control_client_discover(&fixture->conn, NULL); zassert_equal(-EINVAL, err, "Unexpected return value %d", err); } + +static ZTEST_F(ccp_call_control_client_test_suite, test_ccp_call_control_client_get_bearers) +{ + struct bt_ccp_call_control_client_bearers bearers; + int err; + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_discover(&fixture->conn, &fixture->client); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_get_bearers(fixture->client, &bearers); + zassert_equal(0, err, "Unexpected return value %d", err); + +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) + zassert_not_null(bearers.gtbs_bearer); +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ + +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + zassert_equal(CONFIG_BT_TBS_CLIENT_MAX_TBS_INSTANCES, bearers.tbs_count); + zassert_not_null(bearers.tbs_bearers); +#endif /* CONFIG_BT_TBS_CLIENT_TBS */ +} From 07a26bde4647540cd5f4faf50d73cb0e8d6483fe Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Wed, 2 Oct 2024 10:12:19 +0200 Subject: [PATCH 5/5] Bluetooth: CCP: Client: Add support for get provider name Add support for getting the remote bearer provider name. Signed-off-by: Emil Gydesen --- .../bluetooth/shell/audio/ccp.rst | 7 + include/zephyr/bluetooth/audio/ccp.h | 32 ++++ .../ccp_call_control_client/prj.conf | 1 + .../ccp_call_control_client/src/main.c | 74 +++++++- .../bluetooth/audio/ccp_call_control_client.c | 130 ++++++++++++++ .../audio/shell/ccp_call_control_client.c | 122 ++++++++++++- subsys/bluetooth/audio/tbs_internal.h | 5 + .../ccp_call_control_client/CMakeLists.txt | 2 + .../include/ccp_call_control_client.h | 2 + .../include/test_common.h | 17 ++ .../audio/ccp_call_control_client/src/main.c | 18 +- .../ccp_call_control_client/src/test_common.c | 37 ++++ .../src/test_procedures.c | 165 ++++++++++++++++++ .../uut/ccp_call_control_client.c | 7 + .../ccp_call_control_client/uut/tbs_client.c | 14 ++ .../audio/src/ccp_call_control_client_test.c | 59 ++++++- 16 files changed, 666 insertions(+), 26 deletions(-) create mode 100644 tests/bluetooth/audio/ccp_call_control_client/include/test_common.h create mode 100644 tests/bluetooth/audio/ccp_call_control_client/src/test_common.c create mode 100644 tests/bluetooth/audio/ccp_call_control_client/src/test_procedures.c diff --git a/doc/connectivity/bluetooth/shell/audio/ccp.rst b/doc/connectivity/bluetooth/shell/audio/ccp.rst index f5c3be3310a0de..20547f0e255148 100644 --- a/doc/connectivity/bluetooth/shell/audio/ccp.rst +++ b/doc/connectivity/bluetooth/shell/audio/ccp.rst @@ -83,3 +83,10 @@ Example Usage when connected uart:~$ ccp_call_control_client discover Discovery completed with GTBS and 1 TBS bearers + +.. code-block:: console + + uart:~$ ccp_call_control_client read_bearer_name + Bearer 0x20046254 name: Generic TBS + uart:~$ ccp_call_control_client read_bearer_name 1 + Bearer 0x20046256 name: Telephone Bearer #1 diff --git a/include/zephyr/bluetooth/audio/ccp.h b/include/zephyr/bluetooth/audio/ccp.h index 5188f0a30df5e9..63a1824f492621 100644 --- a/include/zephyr/bluetooth/audio/ccp.h +++ b/include/zephyr/bluetooth/audio/ccp.h @@ -168,6 +168,21 @@ struct bt_ccp_call_control_client_cb { void (*discover)(struct bt_ccp_call_control_client *client, int err, struct bt_ccp_call_control_client_bearers *bearers); +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) + /** + * @brief Callback function for bt_ccp_call_control_client_read_bearer_provider_name(). + * + * This callback is called once the read bearer provider name procedure is completed. + * + * @param client Call Control Client instance pointer. + * @param err Error value. 0 on success, GATT error on positive + * value or errno on negative value. + * @param name The bearer provider name. NULL if @p err is not 0. + */ + void (*bearer_provider_name)(struct bt_ccp_call_control_client_bearer *bearer, int err, + const char *name); +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ + /** @internal Internally used field for list handling */ sys_snode_t _node; }; @@ -207,6 +222,23 @@ int bt_ccp_call_control_client_unregister_cb(struct bt_ccp_call_control_client_c int bt_ccp_call_control_client_get_bearers(struct bt_ccp_call_control_client *client, struct bt_ccp_call_control_client_bearers *bearers); +/** + * @brief Read the bearer provider name of a remote TBS bearer. + * + * @kconfig_dep{CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME} + * + * @param bearer The bearer to read the name from + * + * @retval 0 Success + * @retval -EINVAL @p bearer is NULL + * @retval -EFAULT @p bearer has not been discovered + * @retval -EEXIST A @ref bt_ccp_call_control_client could not be identified for @p bearer + * @retval -EBUSY The @ref bt_ccp_call_control_client identified by @p bearer is busy, or the TBS + * instance of @p bearer is busy. + * @retval -ENOTCONN The @ref bt_ccp_call_control_client identified by @p bearer is not connected + */ +int bt_ccp_call_control_client_read_bearer_provider_name( + struct bt_ccp_call_control_client_bearer *bearer); /** @} */ /* End of group bt_ccp_call_control_client */ #ifdef __cplusplus } diff --git a/samples/bluetooth/ccp_call_control_client/prj.conf b/samples/bluetooth/ccp_call_control_client/prj.conf index 85549f2c321c70..e9efbf1fdf7028 100644 --- a/samples/bluetooth/ccp_call_control_client/prj.conf +++ b/samples/bluetooth/ccp_call_control_client/prj.conf @@ -16,6 +16,7 @@ CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT=2 CONFIG_BT_TBS_CLIENT_GTBS=y CONFIG_BT_TBS_CLIENT_TBS=y CONFIG_BT_TBS_CLIENT_MAX_TBS_INSTANCES=1 +CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME=y CONFIG_UTF8=y # TBS Client may require up to 12 buffers diff --git a/samples/bluetooth/ccp_call_control_client/src/main.c b/samples/bluetooth/ccp_call_control_client/src/main.c index db8341204a47f4..50eef5ed93633c 100644 --- a/samples/bluetooth/ccp_call_control_client/src/main.c +++ b/samples/bluetooth/ccp_call_control_client/src/main.c @@ -207,6 +207,21 @@ static void ccp_call_control_client_discover_cb(struct bt_ccp_call_control_clien k_sem_give(&sem_ccp_action_completed); } +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) +static void ccp_call_control_client_read_bearer_provider_name_cb( + struct bt_ccp_call_control_client_bearer *bearer, int err, const char *name) +{ + if (err != 0) { + LOG_ERR("Failed to read bearer %p provider name: %d\n", (void *)bearer, err); + return; + } + + LOG_INF("Bearer %p provider name: %s", (void *)bearer, name); + + k_sem_give(&sem_ccp_action_completed); +} +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ + static int reset_ccp_call_control_client(void) { int err; @@ -259,13 +274,59 @@ static int discover_services(void) return 0; } +static int read_bearer_name(struct bt_ccp_call_control_client_bearer *bearer) +{ + int err; + + err = bt_ccp_call_control_client_read_bearer_provider_name(bearer); + if (err != 0) { + return err; + } + + err = k_sem_take(&sem_ccp_action_completed, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Failed to take sem_ccp_action_completed: %d", err); + return err; + } + + return 0; +} + +static int read_bearer_names(void) +{ + int err; + +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) + err = read_bearer_name(client_bearers.gtbs_bearer); + if (err != 0) { + LOG_ERR("Failed to read name for GTBS bearer: %d", err); + return err; + } +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ + +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + for (size_t i = 0; i < client_bearers.tbs_count; i++) { + err = read_bearer_name(client_bearers.tbs_bearers[i]); + if (err != 0) { + LOG_ERR("Failed to read name for bearer[%zu]: %d", i, err); + return err; + } + } +#endif /* CONFIG_BT_TBS_CLIENT_TBS */ + + return 0; +} + static int init_ccp_call_control_client(void) { - static struct bt_le_scan_cb scan_cbs = { - .recv = scan_recv_cb, - }; static struct bt_ccp_call_control_client_cb ccp_call_control_client_cbs = { .discover = ccp_call_control_client_discover_cb, +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) + .bearer_provider_name = ccp_call_control_client_read_bearer_provider_name_cb +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ + }; + static struct bt_le_scan_cb scan_cbs = { + .recv = scan_recv_cb, }; int err; @@ -323,6 +384,13 @@ int main(void) continue; } + if (IS_ENABLED(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME)) { + err = read_bearer_names(); + if (err != 0) { + continue; + } + } + /* Reset if disconnected */ err = k_sem_take(&sem_conn_state_change, K_FOREVER); if (err != 0) { diff --git a/subsys/bluetooth/audio/ccp_call_control_client.c b/subsys/bluetooth/audio/ccp_call_control_client.c index 91c582597cb74f..a0750dcc7711c5 100644 --- a/subsys/bluetooth/audio/ccp_call_control_client.c +++ b/subsys/bluetooth/audio/ccp_call_control_client.c @@ -27,6 +27,7 @@ LOG_MODULE_REGISTER(bt_ccp_call_control_client, CONFIG_BT_CCP_CALL_CONTROL_CLIEN static sys_slist_t ccp_call_control_client_cbs = SYS_SLIST_STATIC_INIT(&ccp_call_control_client_cbs); +static struct bt_tbs_client_cb tbs_client_cbs; static struct bt_tbs_client_cb tbs_client_cbs; @@ -52,6 +53,32 @@ struct bt_ccp_call_control_client { static struct bt_ccp_call_control_client clients[CONFIG_BT_MAX_CONN]; +static struct bt_ccp_call_control_client_bearer * +get_bearer_by_tbs_index(struct bt_ccp_call_control_client *client, uint8_t index) +{ + for (size_t i = 0U; i < ARRAY_SIZE(client->bearers); i++) { + struct bt_ccp_call_control_client_bearer *bearer = &client->bearers[i]; + + if (bearer->discovered && bearer->tbs_index == index) { + return bearer; + } + } + + return NULL; +} + +static struct bt_ccp_call_control_client * +get_client_by_bearer(const struct bt_ccp_call_control_client_bearer *bearer) +{ + for (size_t i = 0U; i < ARRAY_SIZE(clients); i++) { + if (IS_ARRAY_ELEMENT(clients[i].bearers, bearer)) { + return &clients[i]; + } + } + + return NULL; +} + static struct bt_ccp_call_control_client *get_client_by_conn(const struct bt_conn *conn) { return &clients[bt_conn_index(conn)]; @@ -83,6 +110,8 @@ static void disconnected_cb(struct bt_conn *conn, uint8_t reason) if (client->conn == conn) { bt_conn_unref(client->conn); client->conn = NULL; + + memset(client->bearers, 0, sizeof(client->bearers)); } } @@ -251,3 +280,104 @@ int bt_ccp_call_control_client_get_bearers(struct bt_ccp_call_control_client *cl return 0; } + +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) +static void tbs_client_read_bearer_provider_name_cb(struct bt_conn *conn, int err, + uint8_t inst_index, const char *name) +{ + struct bt_ccp_call_control_client *client = get_client_by_conn(conn); + struct bt_ccp_call_control_client_cb *listener, *next; + struct bt_ccp_call_control_client_bearer *bearer; + + atomic_clear_bit(client->flags, CCP_CALL_CONTROL_CLIENT_FLAG_BUSY); + + bearer = get_bearer_by_tbs_index(client, inst_index); + if (bearer == NULL) { + LOG_DBG("Could not lookup bearer for client %p and index 0x%02X", client, + inst_index); + + return; + } + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&ccp_call_control_client_cbs, listener, next, _node) { + if (listener->bearer_provider_name != NULL) { + listener->bearer_provider_name(bearer, err, name); + } + } +} + +/** + * @brief Validates a bearer and provides a client with ownership of the busy flag + * + * @param[in] bearer The bearer to validate + * @param[out] client A client identified by the @p bearer with the busy flag set if return + * value is 0. + * + * @return 0 if the bearer is valid and the @p client has been populated, else an error. + */ +static int validate_bearer_and_get_client(const struct bt_ccp_call_control_client_bearer *bearer, + struct bt_ccp_call_control_client **client) +{ + CHECKIF(bearer == NULL) { + LOG_DBG("bearer is NULL"); + + return -EINVAL; + } + + *client = get_client_by_bearer(bearer); + if (*client == NULL) { + LOG_DBG("Could not identify client from bearer %p", bearer); + + return -EEXIST; + } + + if (!bearer->discovered) { + LOG_DBG("bearer %p is not discovered", bearer); + + return -EFAULT; + } + + if (atomic_test_and_set_bit((*client)->flags, CCP_CALL_CONTROL_CLIENT_FLAG_BUSY)) { + LOG_DBG("Client %p identified by bearer %p is busy", *client, bearer); + + return -EBUSY; + } + + return 0; +} + +int bt_ccp_call_control_client_read_bearer_provider_name( + struct bt_ccp_call_control_client_bearer *bearer) +{ + struct bt_ccp_call_control_client *client; + int err; + + err = validate_bearer_and_get_client(bearer, &client); + if (err != 0) { + return err; + } + + tbs_client_cbs.bearer_provider_name = tbs_client_read_bearer_provider_name_cb; + + err = bt_tbs_client_read_bearer_provider_name(client->conn, bearer->tbs_index); + if (err != 0) { + atomic_clear_bit(client->flags, CCP_CALL_CONTROL_CLIENT_FLAG_BUSY); + + /* Return expected return values directly */ + if (err == -ENOTCONN || err == -EBUSY) { + LOG_DBG("bt_tbs_client_read_bearer_provider_name returned %d", err); + + return err; + } + + /* Assert if the return value is -EINVAL as that means we are missing a check */ + __ASSERT(err != -EINVAL, "err shall not be -EINVAL"); + + LOG_DBG("Unexpected error from bt_tbs_client_read_bearer_provider_name: %d", err); + + return -ENOEXEC; + } + + return 0; +} +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ diff --git a/subsys/bluetooth/audio/shell/ccp_call_control_client.c b/subsys/bluetooth/audio/shell/ccp_call_control_client.c index c2c57e1bfb43e0..a7929381199fc0 100644 --- a/subsys/bluetooth/audio/shell/ccp_call_control_client.c +++ b/subsys/bluetooth/audio/shell/ccp_call_control_client.c @@ -16,11 +16,18 @@ #include #include #include +#include +#include #include "host/shell/bt.h" static struct bt_ccp_call_control_client *clients[CONFIG_BT_MAX_CONN]; +static struct bt_ccp_call_control_client *get_client_by_conn(const struct bt_conn *conn) +{ + return clients[bt_conn_index(conn)]; +} + static void ccp_call_control_client_discover_cb(struct bt_ccp_call_control_client *client, int err, struct bt_ccp_call_control_client_bearers *bearers) { @@ -38,14 +45,34 @@ static void ccp_call_control_client_discover_cb(struct bt_ccp_call_control_clien tbs_count = bearers->tbs_count; #endif /* CONFIG_BT_TBS_CLIENT_TBS */ - shell_info(ctx_shell, "Discovery completed with %s%u TBS bearers", + shell_info(ctx_shell, "Discovery completed for client %p with %s%u TBS bearers", client, gtbs_bearer != NULL ? "GTBS and " : "", tbs_count); } +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) +static void +ccp_call_control_client_bearer_provider_name_cb(struct bt_ccp_call_control_client_bearer *bearer, + int err, const char *name) +{ + if (err != 0) { + shell_error(ctx_shell, "Failed to read bearer %p name: %d", (void *)bearer, err); + return; + } + + shell_info(ctx_shell, "Bearer %p name: %s", (void *)bearer, name); +} +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ + +static struct bt_ccp_call_control_client_cb ccp_call_control_client_cbs = { + .discover = ccp_call_control_client_discover_cb, +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) + .bearer_provider_name = ccp_call_control_client_bearer_provider_name_cb, +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ +}; + static int cmd_ccp_call_control_client_discover(const struct shell *sh, size_t argc, char *argv[]) { static bool cb_registered; - int err; if (default_conn == NULL) { @@ -58,16 +85,14 @@ static int cmd_ccp_call_control_client_discover(const struct shell *sh, size_t a } if (!cb_registered) { - static struct bt_ccp_call_control_client_cb ccp_call_control_client_cbs = { - .discover = ccp_call_control_client_discover_cb, - }; - err = bt_ccp_call_control_client_register_cb(&ccp_call_control_client_cbs); if (err != 0) { shell_error(sh, "Failed to register CCP Call Control Client cbs (err %d)", err); return -ENOEXEC; } + + cb_registered = true; } err = bt_ccp_call_control_client_discover(default_conn, @@ -81,6 +106,89 @@ static int cmd_ccp_call_control_client_discover(const struct shell *sh, size_t a return 0; } +static int validate_and_get_index(const struct shell *sh, const char *index_arg) +{ + unsigned long index; + int err = 0; + + index = shell_strtoul(index_arg, 0, &err); + if (err != 0) { + shell_error(sh, "Could not parse index: %d", err); + + return -ENOEXEC; + } + + if (index >= CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT) { + shell_error(sh, "Invalid index: %lu", index); + + return -ENOEXEC; + } + + return (int)index; +} + +static struct bt_ccp_call_control_client_bearer *get_bearer_by_index(uint8_t index) +{ + struct bt_ccp_call_control_client_bearers bearers; + struct bt_ccp_call_control_client *client; + int err; + + client = get_client_by_conn(default_conn); + if (client == NULL) { + return NULL; + } + + err = bt_ccp_call_control_client_get_bearers(client, &bearers); + __ASSERT_NO_MSG(err == 0); + +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) && defined(CONFIG_BT_TBS_CLIENT_TBS) + /* If GTBS is enabled then it is at index 0. If the index is not 0, then we decrement it so + * that it can be used as a direct index to the TBS bearers (where index 0 is a TBS inst) + */ + if (index == 0) { + return bearers.gtbs_bearer; + } + index--; +#elif defined(CONFIG_BT_TBS_CLIENT_GTBS) + return bearers.gtbs_bearer; +#endif + +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + return bearers.tbs_bearers[index]; +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ +} + +static int cmd_ccp_call_control_client_read_bearer_name(const struct shell *sh, size_t argc, + char *argv[]) +{ + struct bt_ccp_call_control_client_bearer *bearer; + unsigned long index = 0; + int err; + + if (argc > 1) { + index = validate_and_get_index(sh, argv[1]); + if (index < 0) { + return index; + } + } + + bearer = get_bearer_by_index(index); + if (bearer == NULL) { + shell_error(sh, "Failed to get bearer for index %lu", index); + + return -ENOEXEC; + } + + err = bt_ccp_call_control_client_read_bearer_provider_name(bearer); + if (err != 0) { + shell_error(sh, "Failed to read bearer[%lu] provider name: %d", index, err); + + return -ENOEXEC; + } + + return 0; +} + static int cmd_ccp_call_control_client(const struct shell *sh, size_t argc, char **argv) { if (argc > 1) { @@ -96,6 +204,8 @@ SHELL_STATIC_SUBCMD_SET_CREATE(ccp_call_control_client_cmds, SHELL_CMD_ARG(discover, NULL, "Discover GTBS and TBS on remote device", cmd_ccp_call_control_client_discover, 1, 0), + SHELL_CMD_ARG(read_bearer_name, NULL, "Get bearer name [index]", + cmd_ccp_call_control_client_read_bearer_name, 1, 1), SHELL_SUBCMD_SET_END); SHELL_CMD_ARG_REGISTER(ccp_call_control_client, &ccp_call_control_client_cmds, diff --git a/subsys/bluetooth/audio/tbs_internal.h b/subsys/bluetooth/audio/tbs_internal.h index 68b4dbb0e7004c..3dcfa697e668fb 100644 --- a/subsys/bluetooth/audio/tbs_internal.h +++ b/subsys/bluetooth/audio/tbs_internal.h @@ -319,6 +319,11 @@ enum bt_tbs_client_flag { BT_TBS_CLIENT_FLAG_NUM_FLAGS, /* keep as last */ }; +/* TODO: The storage of calls, handles and parameters should be moved to the user of the TBS client + * (e.g. the CCP client). This allows for users to use the Zephyr CCP client with static allocation + * or implement their own CCP client or even other profile roles that use the TBS client without + * being restricted to static memory allocation + */ struct bt_tbs_instance { struct bt_tbs_client_call_state calls[CONFIG_BT_TBS_CLIENT_MAX_CALLS]; diff --git a/tests/bluetooth/audio/ccp_call_control_client/CMakeLists.txt b/tests/bluetooth/audio/ccp_call_control_client/CMakeLists.txt index 05e846d7878e25..2cee4defb0df49 100644 --- a/tests/bluetooth/audio/ccp_call_control_client/CMakeLists.txt +++ b/tests/bluetooth/audio/ccp_call_control_client/CMakeLists.txt @@ -14,4 +14,6 @@ target_include_directories(testbinary PRIVATE include) target_sources(testbinary PRIVATE src/main.c + src/test_common.c + src/test_procedures.c ) diff --git a/tests/bluetooth/audio/ccp_call_control_client/include/ccp_call_control_client.h b/tests/bluetooth/audio/ccp_call_control_client/include/ccp_call_control_client.h index 998c9f99852bb1..7dca7dc9f341ef 100644 --- a/tests/bluetooth/audio/ccp_call_control_client/include/ccp_call_control_client.h +++ b/tests/bluetooth/audio/ccp_call_control_client/include/ccp_call_control_client.h @@ -21,5 +21,7 @@ void mock_ccp_call_control_client_cleanup(void); DECLARE_FAKE_VOID_FUNC(mock_ccp_call_control_client_discover_cb, struct bt_ccp_call_control_client *, int, struct bt_ccp_call_control_client_bearers *); +DECLARE_FAKE_VOID_FUNC(mock_ccp_call_control_client_bearer_provider_name_cb, + struct bt_ccp_call_control_client_bearer *, int, const char *); #endif /* MOCKS_CCP_CALL_CONTROL_CLIENT_H_ */ diff --git a/tests/bluetooth/audio/ccp_call_control_client/include/test_common.h b/tests/bluetooth/audio/ccp_call_control_client/include/test_common.h new file mode 100644 index 00000000000000..9d45b3a94a11a5 --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/include/test_common.h @@ -0,0 +1,17 @@ +/* test_common.h */ + +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +void test_mocks_init(void); +void test_mocks_cleanup(void); + +void test_conn_init(struct bt_conn *conn); diff --git a/tests/bluetooth/audio/ccp_call_control_client/src/main.c b/tests/bluetooth/audio/ccp_call_control_client/src/main.c index c23d6ed592bb49..5e407384f7d457 100644 --- a/tests/bluetooth/audio/ccp_call_control_client/src/main.c +++ b/tests/bluetooth/audio/ccp_call_control_client/src/main.c @@ -22,6 +22,7 @@ #include "conn.h" #include "ccp_call_control_client.h" #include "expects_util.h" +#include "test_common.h" DEFINE_FFF_GLOBALS; @@ -35,29 +36,16 @@ struct ccp_call_control_client_test_suite_fixture { static void mock_init_rule_before(const struct ztest_unit_test *test, void *fixture) { - mock_ccp_call_control_client_init(); + test_mocks_init(); } static void mock_destroy_rule_after(const struct ztest_unit_test *test, void *fixture) { - mock_ccp_call_control_client_cleanup(); + test_mocks_cleanup(); } ZTEST_RULE(mock_rule, mock_init_rule_before, mock_destroy_rule_after); -static void test_conn_init(struct bt_conn *conn) -{ - conn->index = 0; - conn->info.type = BT_CONN_TYPE_LE; - conn->info.role = BT_CONN_ROLE_CENTRAL; - conn->info.state = BT_CONN_STATE_CONNECTED; - conn->info.security.level = BT_SECURITY_L2; - conn->info.security.enc_key_size = BT_ENC_KEY_SIZE_MAX; - conn->info.security.flags = BT_SECURITY_FLAG_OOB | BT_SECURITY_FLAG_SC; - - mock_bt_conn_connected(conn, BT_HCI_ERR_SUCCESS); -} - static void *ccp_call_control_client_test_suite_setup(void) { struct ccp_call_control_client_test_suite_fixture *fixture; diff --git a/tests/bluetooth/audio/ccp_call_control_client/src/test_common.c b/tests/bluetooth/audio/ccp_call_control_client/src/test_common.c new file mode 100644 index 00000000000000..2f45ab61d7de3b --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/src/test_common.c @@ -0,0 +1,37 @@ +/* common.c - Common functions */ + +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "ccp_call_control_client.h" +#include "conn.h" + +void test_mocks_init(const struct ztest_unit_test *test, void *fixture) +{ + mock_ccp_call_control_client_init(); +} + +void test_mocks_cleanup(const struct ztest_unit_test *test, void *fixture) +{ + mock_ccp_call_control_client_cleanup(); +} + +void test_conn_init(struct bt_conn *conn) +{ + conn->index = 0; + conn->info.type = BT_CONN_TYPE_LE; + conn->info.role = BT_CONN_ROLE_CENTRAL; + conn->info.state = BT_CONN_STATE_CONNECTED; + conn->info.security.level = BT_SECURITY_L2; + conn->info.security.enc_key_size = BT_ENC_KEY_SIZE_MAX; + conn->info.security.flags = BT_SECURITY_FLAG_OOB | BT_SECURITY_FLAG_SC; + + mock_bt_conn_connected(conn, BT_HCI_ERR_SUCCESS); +} diff --git a/tests/bluetooth/audio/ccp_call_control_client/src/test_procedures.c b/tests/bluetooth/audio/ccp_call_control_client/src/test_procedures.c new file mode 100644 index 00000000000000..e0c49eae2e3cec --- /dev/null +++ b/tests/bluetooth/audio/ccp_call_control_client/src/test_procedures.c @@ -0,0 +1,165 @@ +/* test_procedures.c - Testing of CCP procedures */ + +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "conn.h" +#include "ccp_call_control_client.h" +#include "expects_util.h" +#include "test_common.h" + +struct ccp_call_control_client_procedures_test_suite_fixture { + /** Need 1 additional bearer than the max to trigger some corner cases */ + struct bt_ccp_call_control_client_bearer + *bearers[CONFIG_BT_CCP_CALL_CONTROL_CLIENT_BEARER_COUNT]; + struct bt_ccp_call_control_client *client; + struct bt_conn conn; +}; + +static void mock_init_rule_before(const struct ztest_unit_test *test, void *fixture) +{ + test_mocks_init(); +} + +static void mock_destroy_rule_after(const struct ztest_unit_test *test, void *fixture) +{ + test_mocks_cleanup(); +} + +ZTEST_RULE(mock_rule, mock_init_rule_before, mock_destroy_rule_after); + +static void *ccp_call_control_client_procedures_test_suite_setup(void) +{ + struct ccp_call_control_client_procedures_test_suite_fixture *fixture; + + fixture = malloc(sizeof(*fixture)); + zassert_not_null(fixture); + + return fixture; +} + +static void ccp_call_control_client_procedures_test_suite_before(void *f) +{ + struct ccp_call_control_client_procedures_test_suite_fixture *fixture = f; + struct bt_ccp_call_control_client_bearers *bearers; + size_t i = 0U; + int err; + + memset(fixture, 0, sizeof(*fixture)); + test_conn_init(&fixture->conn); + + err = bt_ccp_call_control_client_register_cb(&mock_ccp_call_control_client_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_ccp_call_control_client_discover(&fixture->conn, &fixture->client); + zassert_equal(0, err, "Unexpected return value %d", err); + + zexpect_call_count("bt_ccp_call_control_client_cb.discover", 1, + mock_ccp_call_control_client_discover_cb_fake.call_count); + zassert_not_null(mock_ccp_call_control_client_discover_cb_fake.arg0_history[0]); + zassert_equal(0, mock_ccp_call_control_client_discover_cb_fake.arg1_history[0]); + bearers = mock_ccp_call_control_client_discover_cb_fake.arg2_history[0]; + zassert_not_null(bearers); + +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) + zassert_not_null(bearers->gtbs_bearer); + fixture->bearers[i++] = bearers->gtbs_bearer; +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ + +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + zassert_equal(CONFIG_BT_TBS_CLIENT_MAX_TBS_INSTANCES, bearers->tbs_count); + zassert_not_null(bearers->tbs_bearers); + for (; i < bearers->tbs_count; i++) { + zassert_not_null(bearers->tbs_bearers[i]); + fixture->bearers[i] = bearers->gtbs_bearer; + } +#endif /* CONFIG_BT_TBS_CLIENT_TBS */ +} + +static void ccp_call_control_client_procedures_test_suite_after(void *f) +{ + struct ccp_call_control_client_procedures_test_suite_fixture *fixture = f; + + (void)bt_ccp_call_control_client_unregister_cb(&mock_ccp_call_control_client_cb); + mock_bt_conn_disconnected(&fixture->conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); +} + +static void ccp_call_control_client_procedures_test_suite_teardown(void *f) +{ + free(f); +} + +ZTEST_SUITE(ccp_call_control_client_procedures_test_suite, NULL, + ccp_call_control_client_procedures_test_suite_setup, + ccp_call_control_client_procedures_test_suite_before, + ccp_call_control_client_procedures_test_suite_after, + ccp_call_control_client_procedures_test_suite_teardown); + +static ZTEST_F(ccp_call_control_client_procedures_test_suite, + test_ccp_call_control_client_read_bearer_provider_name) +{ + int err; + + err = bt_ccp_call_control_client_read_bearer_provider_name(fixture->bearers[0]); + zassert_equal(err, 0, "Unexpected return value %d", err); + + zexpect_call_count("bt_ccp_call_control_client_cb.bearer_provider_name", 1, + mock_ccp_call_control_client_bearer_provider_name_cb_fake.call_count); + zassert_not_null(mock_ccp_call_control_client_bearer_provider_name_cb_fake + .arg0_history[0]); /* bearer */ + zassert_equal(0, mock_ccp_call_control_client_bearer_provider_name_cb_fake + .arg1_history[0]); /* err */ + zassert_not_null(mock_ccp_call_control_client_bearer_provider_name_cb_fake + .arg2_history[0]); /* name */ +} + +static ZTEST_F(ccp_call_control_client_procedures_test_suite, + test_ccp_call_control_client_read_bearer_provider_name_inval_null_bearer) +{ + int err; + + err = bt_ccp_call_control_client_read_bearer_provider_name(NULL); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_procedures_test_suite, + test_ccp_call_control_client_read_bearer_provider_name_inval_not_discovered) +{ + int err; + + /* Fake disconnection to clear the discovered value for the bearers*/ + mock_bt_conn_disconnected(&fixture->conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + /* Mark as connected again but without discovering */ + test_conn_init(&fixture->conn); + + err = bt_ccp_call_control_client_read_bearer_provider_name(fixture->bearers[0]); + zassert_equal(err, -EFAULT, "Unexpected return value %d", err); +} + +static ZTEST_F(ccp_call_control_client_procedures_test_suite, + test_ccp_call_control_client_read_bearer_provider_name_inval_bearer) +{ + struct bt_ccp_call_control_client_bearer *invalid_bearer = + (struct bt_ccp_call_control_client_bearer *)0xdeadbeef; + int err; + + err = bt_ccp_call_control_client_read_bearer_provider_name(invalid_bearer); + zassert_equal(err, -EEXIST, "Unexpected return value %d", err); +} diff --git a/tests/bluetooth/audio/ccp_call_control_client/uut/ccp_call_control_client.c b/tests/bluetooth/audio/ccp_call_control_client/uut/ccp_call_control_client.c index 5b4e2363217226..c5bbe670ac883b 100644 --- a/tests/bluetooth/audio/ccp_call_control_client/uut/ccp_call_control_client.c +++ b/tests/bluetooth/audio/ccp_call_control_client/uut/ccp_call_control_client.c @@ -16,9 +16,16 @@ DEFINE_FAKE_VOID_FUNC(mock_ccp_call_control_client_discover_cb, struct bt_ccp_call_control_client *, int, struct bt_ccp_call_control_client_bearers *); +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) +DEFINE_FAKE_VOID_FUNC(mock_ccp_call_control_client_bearer_provider_name_cb, + struct bt_ccp_call_control_client_bearer *, int, const char *); +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ struct bt_ccp_call_control_client_cb mock_ccp_call_control_client_cb = { .discover = mock_ccp_call_control_client_discover_cb, +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) + .bearer_provider_name = mock_ccp_call_control_client_bearer_provider_name_cb, +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ }; void mock_ccp_call_control_client_init(void) diff --git a/tests/bluetooth/audio/ccp_call_control_client/uut/tbs_client.c b/tests/bluetooth/audio/ccp_call_control_client/uut/tbs_client.c index 87564da4587581..9eb86f878373e8 100644 --- a/tests/bluetooth/audio/ccp_call_control_client/uut/tbs_client.c +++ b/tests/bluetooth/audio/ccp_call_control_client/uut/tbs_client.c @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include @@ -32,3 +33,16 @@ int bt_tbs_client_discover(struct bt_conn *conn) return 0; } + +int bt_tbs_client_read_bearer_provider_name(struct bt_conn *conn, uint8_t inst_index) +{ + if (conn == NULL) { + return -ENOTCONN; + } + + if (tbs_cbs != NULL && tbs_cbs->bearer_provider_name != NULL) { + tbs_cbs->bearer_provider_name(conn, 0, inst_index, "bearer name"); + } + + return 0; +} diff --git a/tests/bsim/bluetooth/audio/src/ccp_call_control_client_test.c b/tests/bsim/bluetooth/audio/src/ccp_call_control_client_test.c index 6a7b8019a6037e..fd4b1d62231b32 100644 --- a/tests/bsim/bluetooth/audio/src/ccp_call_control_client_test.c +++ b/tests/bsim/bluetooth/audio/src/ccp_call_control_client_test.c @@ -5,6 +5,7 @@ */ #include #include +#include #include #include @@ -23,8 +24,10 @@ LOG_MODULE_REGISTER(ccp_call_control_client, CONFIG_LOG_DEFAULT_LEVEL); extern enum bst_result_t bst_result; CREATE_FLAG(flag_discovery_complete); +CREATE_FLAG(flag_bearer_name_read); -static struct bt_ccp_call_control_client *inst; +static struct bt_ccp_call_control_client *client; +static struct bt_ccp_call_control_client_bearers client_bearers; static void ccp_call_control_client_discover_cb(struct bt_ccp_call_control_client *client, int err, struct bt_ccp_call_control_client_bearers *bearers) @@ -42,16 +45,33 @@ static void ccp_call_control_client_discover_cb(struct bt_ccp_call_control_clien return; } + memcpy(&client_bearers, bearers, sizeof(client_bearers)); + SET_FLAG(flag_discovery_complete); } +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) +static void ccp_call_control_client_read_bearer_provider_name_cb( + struct bt_ccp_call_control_client_bearer *bearer, int err, const char *name) +{ + if (err != 0) { + FAIL("Failed to read bearer %p provider name: %d\n", (void *)bearer, err); + return; + } + + LOG_INF("Bearer %p provider name: %s", (void *)bearer, name); + + SET_FLAG(flag_bearer_name_read); +} +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ + static void discover_tbs(void) { int err; UNSET_FLAG(flag_discovery_complete); - err = bt_ccp_call_control_client_discover(default_conn, &inst); + err = bt_ccp_call_control_client_discover(default_conn, &client); if (err) { FAIL("Failed to discover TBS: %d", err); return; @@ -60,10 +80,41 @@ static void discover_tbs(void) WAIT_FOR_FLAG(flag_discovery_complete); } +static void read_bearer_name(struct bt_ccp_call_control_client_bearer *bearer) +{ + int err; + + UNSET_FLAG(flag_bearer_name_read); + + err = bt_ccp_call_control_client_read_bearer_provider_name(bearer); + if (err != 0) { + FAIL("Failed to read name of bearer %p: %d", bearer, err); + return; + } + + WAIT_FOR_FLAG(flag_bearer_name_read); +} + +static void read_bearer_names(void) +{ +#if defined(CONFIG_BT_TBS_CLIENT_GTBS) + read_bearer_name(client_bearers.gtbs_bearer); +#endif /* CONFIG_BT_TBS_CLIENT_GTBS */ + +#if defined(CONFIG_BT_TBS_CLIENT_TBS) + for (size_t i = 0; i < client_bearers.tbs_count; i++) { + read_bearer_name(client_bearers.tbs_bearers[i]); + } +#endif /* CONFIG_BT_TBS_CLIENT_TBS */ +} + static void init(void) { static struct bt_ccp_call_control_client_cb ccp_call_control_client_cbs = { .discover = ccp_call_control_client_discover_cb, +#if defined(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME) + .bearer_provider_name = ccp_call_control_client_read_bearer_provider_name_cb +#endif /* CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME */ }; int err; @@ -99,6 +150,10 @@ static void test_main(void) discover_tbs(); discover_tbs(); /* test that we can discover twice */ + if (IS_ENABLED(CONFIG_BT_TBS_CLIENT_BEARER_PROVIDER_NAME)) { + read_bearer_names(); + } + err = bt_conn_disconnect(default_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); if (err != 0) { FAIL("Failed to disconnect: %d\n", err);