blob: 8dc4c44fa5ea11b98ec03a70f80e0d0c285631ed [file] [log] [blame]
/*
* Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA -
* www.ehima.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "state_machine.h"
#include <android_bluetooth_flags.h>
#include <base/functional/bind.h>
#include <base/functional/callback.h>
#include <base/strings/string_number_conversions.h>
#include "bta_gatt_queue.h"
#include "btm_iso_api.h"
#include "client_parser.h"
#include "codec_manager.h"
#include "common/strings.h"
#include "devices.h"
#include "hci/hci_packets.h"
#include "hcimsgs.h"
#include "include/check.h"
#include "internal_include/bt_trace.h"
#include "le_audio_health_status.h"
#include "le_audio_log_history.h"
#include "le_audio_types.h"
#include "os/log.h"
#include "osi/include/alarm.h"
#include "osi/include/osi.h"
#include "osi/include/properties.h"
// clang-format off
/* ASCS state machine 1.0
*
* State machine manages group of ASEs to make transition from one state to
* another according to specification and keeping involved necessary externals
* like: ISO, CIG, ISO data path, audio path form/to upper layer.
*
* GroupStream (API): GroupStream method of this le audio implementation class
* object should allow transition from Idle (No Caching),
* Codec Configured (Caching after release) state to
* Streaming for all ASEs in group within time limit. Time
* limit should keep safe whole state machine from being
* stucked in any in-middle state, which is not a destination
* state.
*
* TODO Second functionality of streaming should be switch
* context which will base on previous state, context type.
*
* GroupStop (API): GroupStop method of this le audio implementation class
* object should allow safe transition from any state to Idle
* or Codec Configured (if caching supported).
*
* ╔══════════════════╦═════════════════════════════╦══════════════╦══════════════════╦══════╗
* ║ Current State ║ ASE Control Point Operation ║ Result ║ Next State ║ Note ║
* ╠══════════════════╬═════════════════════════════╬══════════════╬══════════════════╬══════╣
* ║ Idle ║ Config Codec ║ Success ║ Codec Configured ║ + ║
* ║ Codec Configured ║ Config Codec ║ Success ║ Codec Configured ║ - ║
* ║ Codec Configured ║ Release ║ Success ║ Releasing ║ + ║
* ║ Codec Configured ║ Config QoS ║ Success ║ QoS Configured ║ + ║
* ║ QoS Configured ║ Config Codec ║ Success ║ Codec Configured ║ - ║
* ║ QoS Configured ║ Config QoS ║ Success ║ QoS Configured ║ - ║
* ║ QoS Configured ║ Release ║ Success ║ Releasing ║ + ║
* ║ QoS Configured ║ Enable ║ Success ║ Enabling ║ + ║
* ║ Enabling ║ Release ║ Success ║ Releasing ║ + ║
* ║ Enabling ║ Update Metadata ║ Success ║ Enabling ║ - ║
* ║ Enabling ║ Disable ║ Success ║ Disabling ║ - ║
* ║ Enabling ║ Receiver Start Ready ║ Success ║ Streaming ║ + ║
* ║ Streaming ║ Update Metadata ║ Success ║ Streaming ║ - ║
* ║ Streaming ║ Disable ║ Success ║ Disabling ║ + ║
* ║ Streaming ║ Release ║ Success ║ Releasing ║ + ║
* ║ Disabling ║ Receiver Stop Ready ║ Success ║ QoS Configured ║ + ║
* ║ Disabling ║ Release ║ Success ║ Releasing ║ + ║
* ║ Releasing ║ Released (no caching) ║ Success ║ Idle ║ + ║
* ║ Releasing ║ Released (caching) ║ Success ║ Codec Configured ║ - ║
* ╚══════════════════╩═════════════════════════════╩══════════════╩══════════════════╩══════╝
*
* + - supported transition
* - - not supported
*/
// clang-format on
using bluetooth::common::ToString;
using bluetooth::hci::IsoManager;
using bluetooth::le_audio::GroupStreamStatus;
using le_audio::CodecManager;
using le_audio::LeAudioDevice;
using le_audio::LeAudioDeviceGroup;
using le_audio::LeAudioGroupStateMachine;
using bluetooth::hci::ErrorCode;
using bluetooth::hci::ErrorCodeText;
using le_audio::DsaMode;
using le_audio::DsaModes;
using le_audio::types::ase;
using le_audio::types::AseState;
using le_audio::types::AudioContexts;
using le_audio::types::BidirectionalPair;
using le_audio::types::CigState;
using le_audio::types::CisState;
using le_audio::types::CodecLocation;
using le_audio::types::DataPathState;
using le_audio::types::LeAudioContextType;
using le_audio::types::LeAudioCoreCodecConfig;
namespace {
constexpr int linkQualityCheckInterval = 4000;
constexpr int kAutonomousTransitionTimeoutMs = 5000;
constexpr int kNumberOfCisRetries = 2;
static void link_quality_cb(void* data) {
// very ugly, but we need to pass just two bytes
uint16_t cis_conn_handle = *((uint16_t*)data);
IsoManager::GetInstance()->ReadIsoLinkQuality(cis_conn_handle);
}
class LeAudioGroupStateMachineImpl;
LeAudioGroupStateMachineImpl* instance;
class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
public:
LeAudioGroupStateMachineImpl(Callbacks* state_machine_callbacks_)
: state_machine_callbacks_(state_machine_callbacks_),
watchdog_(alarm_new("LeAudioStateMachineTimer")) {
log_history_ = LeAudioLogHistory::Get();
}
~LeAudioGroupStateMachineImpl() {
alarm_free(watchdog_);
watchdog_ = nullptr;
log_history_->Cleanup();
log_history_ = nullptr;
}
bool AttachToStream(LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
BidirectionalPair<std::vector<uint8_t>> ccids) override {
LOG(INFO) << __func__ << " group id: " << group->group_id_
<< " device: " << ADDRESS_TO_LOGGABLE_STR(leAudioDevice->address_);
/* This function is used to attach the device to the stream.
* Limitation here is that device should be previously in the streaming
* group and just got reconnected.
*/
if (group->GetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING ||
group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
LOG_ERROR(
" group %d no in correct streaming state: %s or target state: %s",
group->group_id_, ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
return false;
}
/* This is cautious - mostly needed for unit test only */
auto group_metadata_contexts =
get_bidirectional(group->GetMetadataContexts());
auto device_available_contexts = leAudioDevice->GetAvailableContexts();
if (!group_metadata_contexts.test_any(device_available_contexts)) {
LOG_INFO("%s does is not have required context type",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
return false;
}
/* Invalidate configuration to make sure it is chosen properly when new
* member connects
*/
group->InvalidateCachedConfigurations();
if (!group->Configure(group->GetConfigurationContextType(),
group->GetMetadataContexts(), ccids)) {
LOG_ERROR(" failed to set ASE configuration");
return false;
}
PrepareAndSendCodecConfigure(group, leAudioDevice);
return true;
}
bool StartStream(
LeAudioDeviceGroup* group, LeAudioContextType context_type,
const BidirectionalPair<AudioContexts>& metadata_context_types,
BidirectionalPair<std::vector<uint8_t>> ccid_lists) override {
LOG_INFO(" current state: %s", ToString(group->GetState()).c_str());
switch (group->GetState()) {
case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
if (group->IsConfiguredForContext(context_type)) {
if (group->Activate(context_type, metadata_context_types,
ccid_lists)) {
SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
if (CigCreate(group)) {
return true;
}
}
LOG_INFO("Could not activate device, try to configure it again");
}
/* Deactivate previousely activated ASEs in case if there were just a
* reconfiguration (group target state as CODEC CONFIGURED) and no
* deactivation. Currently activated ASEs cannot be used for different
* context.
*/
group->Deactivate();
/* We are going to reconfigure whole group. Clear Cises.*/
ReleaseCisIds(group);
/* If configuration is needed */
FALLTHROUGH_INTENDED;
case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
if (!group->Configure(context_type, metadata_context_types,
ccid_lists)) {
LOG(ERROR) << __func__ << ", failed to set ASE configuration";
return false;
}
group->cig.GenerateCisIds(context_type);
/* All ASEs should aim to achieve target state */
SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
if (!PrepareAndSendCodecConfigToTheGroup(group)) {
group->PrintDebugState();
ClearGroup(group, true);
}
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED: {
LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
if (!leAudioDevice) {
LOG(ERROR) << __func__ << ", group has no active devices";
return false;
}
/* All ASEs should aim to achieve target state */
SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
PrepareAndSendEnableToTheGroup(group);
break;
}
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING: {
/* This case just updates the metadata for the stream, in case
* stream configuration is satisfied. We can do that already for
* all the devices in a group, without any state transitions.
*/
if (!group->IsMetadataChanged(metadata_context_types, ccid_lists))
return true;
LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
if (!leAudioDevice) {
LOG(ERROR) << __func__ << ", group has no active devices";
return false;
}
while (leAudioDevice) {
PrepareAndSendUpdateMetadata(leAudioDevice, metadata_context_types,
ccid_lists);
leAudioDevice = group->GetNextActiveDevice(leAudioDevice);
}
break;
}
default:
LOG_ERROR("Unable to transit from %s",
ToString(group->GetState()).c_str());
return false;
}
return true;
}
bool ConfigureStream(
LeAudioDeviceGroup* group, LeAudioContextType context_type,
const BidirectionalPair<AudioContexts>& metadata_context_types,
BidirectionalPair<std::vector<uint8_t>> ccid_lists) override {
if (group->GetState() > AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED) {
LOG_ERROR(
"Stream should be stopped or in configured stream. Current state: %s",
ToString(group->GetState()).c_str());
return false;
}
group->Deactivate();
ReleaseCisIds(group);
if (!group->Configure(context_type, metadata_context_types, ccid_lists)) {
LOG_ERROR("Could not configure ASEs for group %d content type %d",
group->group_id_, int(context_type));
return false;
}
group->cig.GenerateCisIds(context_type);
SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
return PrepareAndSendCodecConfigToTheGroup(group);
}
void SuspendStream(LeAudioDeviceGroup* group) override {
/* All ASEs should aim to achieve target state */
SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
auto status = PrepareAndSendDisableToTheGroup(group);
state_machine_callbacks_->StatusReportCb(group->group_id_, status);
}
void StopStream(LeAudioDeviceGroup* group) override {
if (group->IsReleasingOrIdle()) {
LOG(INFO) << __func__ << ", group: " << group->group_id_
<< " already in releasing process";
return;
}
/* All Ases should aim to achieve target state */
SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
auto status = PrepareAndSendReleaseToTheGroup(group);
state_machine_callbacks_->StatusReportCb(group->group_id_, status);
}
void notifyLeAudioHealth(LeAudioDeviceGroup* group,
le_audio::LeAudioHealthGroupStatType stat) {
if (!IS_FLAG_ENABLED(leaudio_enable_health_based_actions)) {
return;
}
auto leAudioHealthStatus = le_audio::LeAudioHealthStatus::Get();
if (leAudioHealthStatus) {
leAudioHealthStatus->AddStatisticForGroup(group, stat);
}
}
void ProcessGattCtpNotification(LeAudioDeviceGroup* group, uint8_t* value,
uint16_t len) {
auto ntf =
std::make_unique<struct le_audio::client_parser::ascs::ctp_ntf>();
bool valid_notification = ParseAseCtpNotification(*ntf, len, value);
if (group == nullptr) {
LOG_WARN("Notification received to invalid group");
return;
}
/* State machine looks on ASE state and base on it take decisions.
* If ASE state is not achieve on time, timeout is reported and upper
* layer mostlikely drops ACL considers that remote is in bad state.
* However, it might happen that remote device rejects ASE configuration for
* some reason and ASCS specification defines tones of different reasons.
* Maybe in the future we will be able to handle all of them but for now it
* seems to be important to allow remote device to reject ASE configuration
* when stream is creating. e.g. Allow remote to reject Enable on unwanted
* context type.
*/
auto target_state = group->GetTargetState();
auto in_transition = group->IsInTransition();
if (!in_transition ||
target_state != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
LOG_DEBUG(
"Not interested in ctp result for group %d inTransistion: %d , "
"targetState: %s",
group->group_id_, in_transition, ToString(target_state).c_str());
return;
}
if (!valid_notification) {
/* Do nothing, just allow guard timer to fire */
LOG_ERROR("Invalid CTP notification for group %d", group->group_id_);
return;
}
for (auto& entry : ntf->entries) {
if (entry.response_code !=
le_audio::client_parser::ascs::kCtpResponseCodeSuccess) {
/* Gracefully stop the stream */
LOG_ERROR(
"Stoping stream due to control point error for ase: %d, error: "
"0x%02x, reason: 0x%02x",
entry.ase_id, entry.response_code, entry.reason);
notifyLeAudioHealth(group, le_audio::LeAudioHealthGroupStatType::
STREAM_CREATE_SIGNALING_FAILED);
StopStream(group);
return;
}
}
LOG_DEBUG(
"Ctp result OK for group %d inTransistion: %d , "
"targetState: %s",
group->group_id_, in_transition, ToString(target_state).c_str());
}
void ProcessGattNotifEvent(uint8_t* value, uint16_t len, struct ase* ase,
LeAudioDevice* leAudioDevice,
LeAudioDeviceGroup* group) override {
struct le_audio::client_parser::ascs::ase_rsp_hdr arh;
ParseAseStatusHeader(arh, len, value);
if (ase->id == 0x00) {
/* Initial state of Ase - update id */
LOG_INFO(", discovered ase id: %d", arh.id);
ase->id = arh.id;
}
auto state = static_cast<AseState>(arh.state);
LOG_INFO(" %s , ASE id: %d, state changed %s -> %s ",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), +ase->id,
ToString(ase->state).c_str(), ToString(state).c_str());
log_history_->AddLogHistory(
kLogAseStateNotif, leAudioDevice->group_id_, leAudioDevice->address_,
"ASE_ID " + std::to_string(arh.id) + ": " + ToString(state),
"curr: " + ToString(ase->state));
switch (state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
AseStateMachineProcessIdle(arh, ase, group, leAudioDevice);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
AseStateMachineProcessCodecConfigured(
arh, ase, value + le_audio::client_parser::ascs::kAseRspHdrMinLen,
len - le_audio::client_parser::ascs::kAseRspHdrMinLen, group,
leAudioDevice);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
AseStateMachineProcessQosConfigured(arh, ase, group, leAudioDevice);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
AseStateMachineProcessEnabling(arh, ase, group, leAudioDevice);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
AseStateMachineProcessStreaming(
arh, ase, value + le_audio::client_parser::ascs::kAseRspHdrMinLen,
len - le_audio::client_parser::ascs::kAseRspHdrMinLen, group,
leAudioDevice);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING:
AseStateMachineProcessDisabling(arh, ase, group, leAudioDevice);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING:
AseStateMachineProcessReleasing(arh, ase, group, leAudioDevice);
break;
default:
LOG(ERROR) << __func__
<< ", Wrong AES status: " << static_cast<int>(arh.state);
StopStream(group);
break;
}
}
void ProcessHciNotifOnCigCreate(LeAudioDeviceGroup* group, uint8_t status,
uint8_t cig_id,
std::vector<uint16_t> conn_handles) override {
/* TODO: What if not all cises will be configured ?
* conn_handle.size() != active ases in group
*/
if (!group) {
LOG_ERROR(", group is null");
return;
}
log_history_->AddLogHistory(kLogHciEvent, group->group_id_,
RawAddress::kEmpty,
kLogCisCreateOp + "STATUS=" + loghex(status));
if (status != HCI_SUCCESS) {
if (status == HCI_ERR_COMMAND_DISALLOWED) {
/*
* We are here, because stack has no chance to remove CIG when it was
* shut during streaming. In the same time, controller probably was not
* Reseted, which creates the issue. Lets remove CIG and try to create
* it again.
*/
group->cig.SetState(CigState::RECOVERING);
IsoManager::GetInstance()->RemoveCig(group->group_id_, true);
return;
}
group->cig.SetState(CigState::NONE);
LOG_ERROR(", failed to create CIG, reason: 0x%02x, new cig state: %s",
+status, ToString(group->cig.GetState()).c_str());
StopStream(group);
return;
}
ASSERT_LOG(group->cig.GetState() == CigState::CREATING,
"Unexpected CIG creation group id: %d, cig state: %s",
group->group_id_, ToString(group->cig.GetState()).c_str());
group->cig.SetState(CigState::CREATED);
LOG_INFO("Group: %p, id: %d cig state: %s, number of cis handles: %d",
group, group->group_id_, ToString(group->cig.GetState()).c_str(),
static_cast<int>(conn_handles.size()));
/* Assign all connection handles to CIS ids of the CIG */
group->cig.AssignCisConnHandles(conn_handles);
/* Assign all connection handles to multiple device ASEs */
group->AssignCisConnHandlesToAses();
/* Last node configured, process group to codec configured state */
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
PrepareAndSendQoSToTheGroup(group);
} else {
LOG_ERROR(", invalid state transition, from: %s , to: %s",
ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
StopStream(group);
return;
}
}
void FreeLinkQualityReports(LeAudioDevice* leAudioDevice) {
if (leAudioDevice->link_quality_timer == nullptr) return;
alarm_free(leAudioDevice->link_quality_timer);
leAudioDevice->link_quality_timer = nullptr;
}
void ProcessHciNotifyOnCigRemoveRecovering(uint8_t status,
LeAudioDeviceGroup* group) {
group->cig.SetState(CigState::NONE);
log_history_->AddLogHistory(kLogHciEvent, group->group_id_,
RawAddress::kEmpty,
kLogCigRemoveOp + " STATUS=" + loghex(status));
if (status != HCI_SUCCESS) {
LOG_ERROR(
"Could not recover from the COMMAND DISALLOAD on CigCreate. Status "
"on CIG remove is 0x%02x",
status);
StopStream(group);
return;
}
LOG_INFO("Succeed on CIG Recover - back to creating CIG");
if (!CigCreate(group)) {
LOG_ERROR("Could not create CIG. Stop the stream for group %d",
group->group_id_);
StopStream(group);
}
}
void ProcessHciNotifOnCigRemove(uint8_t status,
LeAudioDeviceGroup* group) override {
if (group->cig.GetState() == CigState::RECOVERING) {
ProcessHciNotifyOnCigRemoveRecovering(status, group);
return;
}
log_history_->AddLogHistory(kLogHciEvent, group->group_id_,
RawAddress::kEmpty,
kLogCigRemoveOp + " STATUS=" + loghex(status));
if (status != HCI_SUCCESS) {
group->cig.SetState(CigState::CREATED);
LOG_ERROR(
"failed to remove cig, id: %d, status 0x%02x, new cig state: %s",
group->group_id_, +status, ToString(group->cig.GetState()).c_str());
return;
}
ASSERT_LOG(group->cig.GetState() == CigState::REMOVING,
"Unexpected CIG remove group id: %d, cig state %s",
group->group_id_, ToString(group->cig.GetState()).c_str());
group->cig.SetState(CigState::NONE);
LeAudioDevice* leAudioDevice = group->GetFirstDevice();
if (!leAudioDevice) return;
do {
FreeLinkQualityReports(leAudioDevice);
for (auto& ase : leAudioDevice->ases_) {
ase.cis_state = CisState::IDLE;
ase.data_path_state = DataPathState::IDLE;
}
} while ((leAudioDevice = group->GetNextDevice(leAudioDevice)));
}
void ProcessHciNotifSetupIsoDataPath(LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice,
uint8_t status,
uint16_t conn_handle) override {
log_history_->AddLogHistory(
kLogHciEvent, group->group_id_, leAudioDevice->address_,
kLogSetDataPathOp + "cis_h:" + loghex(conn_handle) +
" STATUS=" + loghex(status));
if (status) {
LOG(ERROR) << __func__ << ", failed to setup data path";
StopStream(group);
return;
}
if (IS_FLAG_ENABLED(leaudio_dynamic_spatial_audio)) {
if (group->dsa_.active &&
(group->dsa_.mode == DsaMode::ISO_SW ||
group->dsa_.mode == DsaMode::ISO_HW) &&
leAudioDevice->GetDsaDataPathState() == DataPathState::CONFIGURING) {
LOG_INFO("Datapath configured for headtracking");
leAudioDevice->SetDsaDataPathState(DataPathState::CONFIGURED);
return;
}
}
/* Update state for the given cis.*/
auto ase = leAudioDevice->GetFirstActiveAseByCisAndDataPathState(
CisState::CONNECTED, DataPathState::CONFIGURING);
if (!ase || ase->cis_conn_hdl != conn_handle) {
LOG(ERROR) << __func__ << " Cannot find ase by handle " << +conn_handle;
return;
}
ase->data_path_state = DataPathState::CONFIGURED;
if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
LOG(WARNING) << __func__ << " Group " << group->group_id_
<< " is not targeting streaming state any more";
return;
}
AddCisToStreamConfiguration(group, ase);
if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING &&
!group->GetFirstActiveDeviceByCisAndDataPathState(
CisState::CONNECTED, DataPathState::IDLE)) {
/* No more transition for group. Here we are for the late join device
* scenario */
cancel_watchdog_if_needed(group->group_id_);
}
if (group->GetNotifyStreamingWhenCisesAreReadyFlag() &&
group->IsGroupStreamReady()) {
group->SetNotifyStreamingWhenCisesAreReadyFlag(false);
LOG_INFO("Ready to notify Group Streaming.");
cancel_watchdog_if_needed(group->group_id_);
if (group->GetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
}
state_machine_callbacks_->StatusReportCb(group->group_id_,
GroupStreamStatus::STREAMING);
};
}
void ProcessHciNotifRemoveIsoDataPath(LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice,
uint8_t status,
uint16_t conn_hdl) override {
log_history_->AddLogHistory(
kLogHciEvent, group->group_id_, leAudioDevice->address_,
kLogRemoveDataPathOp + "STATUS=" + loghex(status));
if (status != HCI_SUCCESS) {
LOG_ERROR(
"failed to remove ISO data path, reason: 0x%0x - contining stream "
"closing",
status);
/* Just continue - disconnecting CIS removes data path as well.*/
}
bool do_disconnect = false;
auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(conn_hdl);
if (ases_pair.sink &&
(ases_pair.sink->data_path_state == DataPathState::REMOVING)) {
ases_pair.sink->data_path_state = DataPathState::IDLE;
if (ases_pair.sink->cis_state == CisState::CONNECTED) {
ases_pair.sink->cis_state = CisState::DISCONNECTING;
do_disconnect = true;
}
}
if (ases_pair.source &&
(ases_pair.source->data_path_state == DataPathState::REMOVING)) {
ases_pair.source->data_path_state = DataPathState::IDLE;
if (ases_pair.source->cis_state == CisState::CONNECTED) {
ases_pair.source->cis_state = CisState::DISCONNECTING;
do_disconnect = true;
}
} else if (IS_FLAG_ENABLED(leaudio_dynamic_spatial_audio)) {
if (group->dsa_.active &&
leAudioDevice->GetDsaDataPathState() == DataPathState::REMOVING) {
LOG_INFO("DSA data path removed");
leAudioDevice->SetDsaDataPathState(DataPathState::IDLE);
leAudioDevice->SetDsaCisHandle(GATT_INVALID_CONN_ID);
}
}
if (do_disconnect) {
group->RemoveCisFromStreamIfNeeded(leAudioDevice, conn_hdl);
IsoManager::GetInstance()->DisconnectCis(conn_hdl, HCI_ERR_PEER_USER);
log_history_->AddLogHistory(
kLogStateMachineTag, group->group_id_, leAudioDevice->address_,
kLogCisDisconnectOp + "cis_h:" + loghex(conn_hdl));
}
}
void ProcessHciNotifIsoLinkQualityRead(
LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
uint8_t conn_handle, uint32_t txUnackedPackets, uint32_t txFlushedPackets,
uint32_t txLastSubeventPackets, uint32_t retransmittedPackets,
uint32_t crcErrorPackets, uint32_t rxUnreceivedPackets,
uint32_t duplicatePackets) {
LOG(INFO) << "conn_handle: " << loghex(conn_handle)
<< ", txUnackedPackets: " << loghex(txUnackedPackets)
<< ", txFlushedPackets: " << loghex(txFlushedPackets)
<< ", txLastSubeventPackets: " << loghex(txLastSubeventPackets)
<< ", retransmittedPackets: " << loghex(retransmittedPackets)
<< ", crcErrorPackets: " << loghex(crcErrorPackets)
<< ", rxUnreceivedPackets: " << loghex(rxUnreceivedPackets)
<< ", duplicatePackets: " << loghex(duplicatePackets);
}
void ReleaseCisIds(LeAudioDeviceGroup* group) {
if (group == nullptr) {
LOG_DEBUG(" Group is null.");
return;
}
LOG_DEBUG(" Releasing CIS is for group %d", group->group_id_);
LeAudioDevice* leAudioDevice = group->GetFirstDevice();
while (leAudioDevice != nullptr) {
for (auto& ase : leAudioDevice->ases_) {
ase.cis_id = le_audio::kInvalidCisId;
ase.cis_conn_hdl = 0;
}
leAudioDevice = group->GetNextDevice(leAudioDevice);
}
group->ClearAllCises();
}
void RemoveCigForGroup(LeAudioDeviceGroup* group) {
LOG_DEBUG("Group: %p, id: %d cig state: %s", group, group->group_id_,
ToString(group->cig.GetState()).c_str());
if (group->cig.GetState() != CigState::CREATED) {
LOG_WARN("Group: %p, id: %d cig state: %s cannot be removed", group,
group->group_id_, ToString(group->cig.GetState()).c_str());
return;
}
group->cig.SetState(CigState::REMOVING);
IsoManager::GetInstance()->RemoveCig(group->group_id_);
LOG_DEBUG("Group: %p, id: %d cig state: %s", group, group->group_id_,
ToString(group->cig.GetState()).c_str());
log_history_->AddLogHistory(kLogStateMachineTag, group->group_id_,
RawAddress::kEmpty, kLogCigRemoveOp);
}
void ProcessHciNotifAclDisconnected(LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice) {
FreeLinkQualityReports(leAudioDevice);
if (!group) {
LOG(ERROR) << __func__
<< " group is null for device: "
<< ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_)
<< " group_id: " << leAudioDevice->group_id_;
/* mark ASEs as not used. */
leAudioDevice->DeactivateAllAses();
return;
}
/* It is possible that ACL disconnection came before CIS disconnect event */
for (auto& ase : leAudioDevice->ases_) {
group->RemoveCisFromStreamIfNeeded(leAudioDevice, ase.cis_conn_hdl);
}
/* mark ASEs as not used. */
leAudioDevice->DeactivateAllAses();
/* Update the current group audio context availability which could change
* due to disconnected group member.
*/
group->ReloadAudioLocations();
group->ReloadAudioDirections();
group->UpdateAudioContextAvailability();
group->InvalidateCachedConfigurations();
/* If group is in Idle and not transitioning, update the current group
* audio context availability which could change due to disconnected group
* member.
*/
if ((group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) &&
!group->IsInTransition()) {
LOG_INFO("group: %d is in IDLE", group->group_id_);
/* When OnLeAudioDeviceSetStateTimeout happens, group will transition
* to IDLE, and after that an ACL disconnect will be triggered. We need
* to check if CIG is created and if it is, remove it so it can be created
* again after reconnect. Otherwise we will get Command Disallowed on CIG
* Create when starting stream.
*/
if (group->cig.GetState() == CigState::CREATED) {
LOG_INFO("CIG is in CREATED state so removing CIG for Group %d",
group->group_id_);
RemoveCigForGroup(group);
}
return;
}
LOG_DEBUG(
" device: %s, group connected: %d, all active ase disconnected:: %d",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_),
group->IsAnyDeviceConnected(), group->HaveAllCisesDisconnected());
if (group->IsAnyDeviceConnected()) {
/*
* ACL of one of the device has been dropped. If number of CISes has
* changed notify upper layer so the CodecManager can be updated with CIS
* information.
*/
if (!group->HaveAllCisesDisconnected()) {
/* some CISes are connected */
if ((group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) &&
(group->GetTargetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING)) {
/* We keep streaming but want others to let know user that it might
* be need to update CodecManager with new CIS configuration
*/
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::STREAMING);
} else {
LOG_WARN("group_id %d not in streaming, CISes are still there",
group->group_id_);
group->PrintDebugState();
}
return;
}
}
/* Group is not connected and all the CISes are down.
* Clean states and destroy HCI group
*/
ClearGroup(group, true);
}
void cancel_watchdog_if_needed(int group_id) {
if (alarm_is_scheduled(watchdog_)) {
log_history_->AddLogHistory(kLogStateMachineTag, group_id,
RawAddress::kEmpty, "WATCHDOG STOPPED");
alarm_cancel(watchdog_);
}
}
void applyDsaDataPath(LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
uint16_t conn_hdl) {
if (!IS_FLAG_ENABLED(leaudio_dynamic_spatial_audio)) {
return;
}
if (!group->dsa_.active) {
LOG_INFO("DSA mode not used");
return;
}
DsaModes dsa_modes = leAudioDevice->GetDsaModes();
if (dsa_modes.empty()) {
LOG_WARN("DSA mode not supported by this LE Audio device: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
group->dsa_.active = false;
return;
}
if (std::find(dsa_modes.begin(), dsa_modes.end(), DsaMode::ISO_SW) ==
dsa_modes.end() &&
std::find(dsa_modes.begin(), dsa_modes.end(), DsaMode::ISO_HW) ==
dsa_modes.end()) {
LOG_WARN("DSA mode not supported by this LE Audio device: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
group->dsa_.active = false;
return;
}
uint8_t data_path_id = bluetooth::hci::iso_manager::kIsoDataPathHci;
LOG_INFO("DSA mode used: %d", static_cast<int>(group->dsa_.mode));
switch (group->dsa_.mode) {
case DsaMode::ISO_HW:
data_path_id = bluetooth::hci::iso_manager::kIsoDataPathPlatformDefault;
break;
case DsaMode::ISO_SW:
data_path_id = bluetooth::hci::iso_manager::kIsoDataPathHci;
break;
default:
LOG_WARN("Unexpected DsaMode: %d", static_cast<int>(group->dsa_.mode));
group->dsa_.active = false;
return;
}
leAudioDevice->SetDsaDataPathState(DataPathState::CONFIGURING);
leAudioDevice->SetDsaCisHandle(conn_hdl);
LOG_VERBOSE(
"DSA mode supported on this LE Audio device: %s, apply data path: %d",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), data_path_id);
LeAudioLogHistory::Get()->AddLogHistory(
kLogStateMachineTag, group->group_id_, RawAddress::kEmpty,
kLogSetDataPathOp + "cis_h:" + loghex(conn_hdl),
"direction: " +
loghex(bluetooth::hci::iso_manager::kIsoDataPathDirectionOut));
bluetooth::hci::iso_manager::iso_data_path_params param = {
.data_path_dir = bluetooth::hci::iso_manager::kIsoDataPathDirectionOut,
.data_path_id = data_path_id,
.codec_id_format =
le_audio::types::kLeAudioCodecHeadtracking.coding_format,
.codec_id_company =
le_audio::types::kLeAudioCodecHeadtracking.vendor_company_id,
.codec_id_vendor =
le_audio::types::kLeAudioCodecHeadtracking.vendor_codec_id,
.controller_delay = 0x00000000,
.codec_conf = std::vector<uint8_t>(),
};
IsoManager::GetInstance()->SetupIsoDataPath(conn_hdl, std::move(param));
}
void ProcessHciNotifCisEstablished(
LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
const bluetooth::hci::iso_manager::cis_establish_cmpl_evt* event)
override {
auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(event->cis_conn_hdl);
log_history_->AddLogHistory(
kLogHciEvent, group->group_id_, leAudioDevice->address_,
kLogCisEstablishedOp + "cis_h:" + loghex(event->cis_conn_hdl) +
" STATUS=" + loghex(event->status));
if (event->status != HCI_SUCCESS) {
if (ases_pair.sink) ases_pair.sink->cis_state = CisState::ASSIGNED;
if (ases_pair.source) ases_pair.source->cis_state = CisState::ASSIGNED;
LOG_WARN("%s: failed to create CIS 0x%04x, status: %s (0x%02x)",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_),
event->cis_conn_hdl,
ErrorCodeText((ErrorCode)event->status).c_str(), event->status);
if (event->status == HCI_ERR_CONN_FAILED_ESTABLISHMENT &&
((leAudioDevice->cis_failed_to_be_established_retry_cnt_++) <
kNumberOfCisRetries) &&
(CisCreateForDevice(group, leAudioDevice))) {
LOG_INFO("Retrying (%d) to create CIS for %s ",
leAudioDevice->cis_failed_to_be_established_retry_cnt_,
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
return;
}
if (event->status == HCI_ERR_UNSUPPORTED_REM_FEATURE &&
group->asymmetric_phy_for_unidirectional_cis_supported == true &&
group->GetSduInterval(le_audio::types::kLeAudioDirectionSource) ==
0) {
group->asymmetric_phy_for_unidirectional_cis_supported = false;
}
LOG_ERROR("CIS creation failed %d times, stopping the stream",
leAudioDevice->cis_failed_to_be_established_retry_cnt_);
leAudioDevice->cis_failed_to_be_established_retry_cnt_ = 0;
/* CIS establishment failed. Remove CIG if no other CIS is already created
* or pending. If CIS is established, this will be handled in disconnected
* complete event
*/
if (group->HaveAllCisesDisconnected()) {
RemoveCigForGroup(group);
}
StopStream(group);
return;
}
if (leAudioDevice->cis_failed_to_be_established_retry_cnt_ > 0) {
/* Reset retry counter */
leAudioDevice->cis_failed_to_be_established_retry_cnt_ = 0;
}
if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
LOG_ERROR("Unintended CIS establishement event came for group id: %d",
group->group_id_);
StopStream(group);
return;
}
if (ases_pair.sink) ases_pair.sink->cis_state = CisState::CONNECTED;
if (ases_pair.source) ases_pair.source->cis_state = CisState::CONNECTED;
if (ases_pair.sink &&
(ases_pair.sink->data_path_state == DataPathState::IDLE)) {
PrepareDataPath(group->group_id_, ases_pair.sink);
}
if (ases_pair.source &&
(ases_pair.source->data_path_state == DataPathState::IDLE)) {
PrepareDataPath(group->group_id_, ases_pair.source);
} else {
applyDsaDataPath(group, leAudioDevice, event->cis_conn_hdl);
}
if (osi_property_get_bool("persist.bluetooth.iso_link_quality_report",
false)) {
leAudioDevice->link_quality_timer =
alarm_new_periodic("le_audio_cis_link_quality");
leAudioDevice->link_quality_timer_data = event->cis_conn_hdl;
alarm_set_on_mloop(leAudioDevice->link_quality_timer,
linkQualityCheckInterval, link_quality_cb,
&leAudioDevice->link_quality_timer_data);
}
if (!leAudioDevice->HaveAllActiveAsesCisEst()) {
/* More cis established events has to come */
return;
}
if (!leAudioDevice->IsReadyToCreateStream()) {
/* Device still remains in ready to create stream state. It means that
* more enabling status notifications has to come. This may only happen
* for reconnection scenario for bi-directional CIS.
*/
return;
}
/* All CISes created. Send start ready for source ASE before we can go
* to streaming state.
*/
struct ase* ase = leAudioDevice->GetFirstActiveAse();
ASSERT_LOG(ase != nullptr,
"shouldn't be called without an active ASE, device %s, group "
"id: %d, cis handle 0x%04x",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), event->cig_id,
event->cis_conn_hdl);
PrepareAndSendReceiverStartReady(leAudioDevice, ase);
}
static void WriteToControlPoint(LeAudioDevice* leAudioDevice,
std::vector<uint8_t> value) {
tGATT_WRITE_TYPE write_type = GATT_WRITE_NO_RSP;
if (value.size() > (leAudioDevice->mtu_ - 3)) {
LOG_WARN("%s, using long write procedure (%d > %d)",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_),
static_cast<int>(value.size()), (leAudioDevice->mtu_ - 3));
/* Note, that this type is actually LONG WRITE.
* Meaning all the Prepare Writes plus Execute is handled in the stack
*/
write_type = GATT_WRITE_PREPARE;
}
BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_,
leAudioDevice->ctp_hdls_.val_hdl, value,
write_type, NULL, NULL);
}
static void RemoveDataPathByCisHandle(LeAudioDevice* leAudioDevice,
uint16_t cis_conn_hdl) {
auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(cis_conn_hdl);
uint8_t value = 0;
if (ases_pair.sink &&
ases_pair.sink->data_path_state == DataPathState::CONFIGURED) {
value |= bluetooth::hci::iso_manager::kRemoveIsoDataPathDirectionInput;
ases_pair.sink->data_path_state = DataPathState::REMOVING;
}
if (ases_pair.source &&
ases_pair.source->data_path_state == DataPathState::CONFIGURED) {
value |= bluetooth::hci::iso_manager::kRemoveIsoDataPathDirectionOutput;
ases_pair.source->data_path_state = DataPathState::REMOVING;
} else {
if (IS_FLAG_ENABLED(leaudio_dynamic_spatial_audio)) {
if (leAudioDevice->GetDsaDataPathState() == DataPathState::CONFIGURED) {
value |=
bluetooth::hci::iso_manager::kRemoveIsoDataPathDirectionOutput;
leAudioDevice->SetDsaDataPathState(DataPathState::REMOVING);
}
}
}
if (value == 0) {
LOG_INFO("Data path was not set. Nothing to do here.");
return;
}
IsoManager::GetInstance()->RemoveIsoDataPath(cis_conn_hdl, value);
LeAudioLogHistory::Get()->AddLogHistory(
kLogStateMachineTag, leAudioDevice->group_id_, leAudioDevice->address_,
kLogRemoveDataPathOp + " cis_h:" + loghex(cis_conn_hdl));
}
void ProcessHciNotifCisDisconnected(
LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice,
const bluetooth::hci::iso_manager::cis_disconnected_evt* event) override {
/* Reset the disconnected CIS states */
FreeLinkQualityReports(leAudioDevice);
auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(event->cis_conn_hdl);
log_history_->AddLogHistory(
kLogHciEvent, group->group_id_, leAudioDevice->address_,
kLogCisDisconnectedOp + "cis_h:" + loghex(event->cis_conn_hdl) +
" REASON=" + loghex(event->reason));
if (ases_pair.sink) {
ases_pair.sink->cis_state = CisState::ASSIGNED;
}
if (ases_pair.source) {
ases_pair.source->cis_state = CisState::ASSIGNED;
}
RemoveDataPathByCisHandle(leAudioDevice, event->cis_conn_hdl);
/* If this is peer disconnecting CIS, make sure to clear data path */
if (event->reason != HCI_ERR_CONN_CAUSE_LOCAL_HOST) {
// Make sure we won't stay in STREAMING state
if (ases_pair.sink &&
ases_pair.sink->state == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
SetAseState(leAudioDevice, ases_pair.sink,
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
}
if (ases_pair.source && ases_pair.source->state ==
AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
SetAseState(leAudioDevice, ases_pair.source,
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
}
}
group->RemoveCisFromStreamIfNeeded(leAudioDevice, event->cis_conn_hdl);
auto target_state = group->GetTargetState();
switch (target_state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
/* Something wrong happen when streaming or when creating stream.
* If there is other device connected and streaming, just leave it as it
* is, otherwise stop the stream.
*/
if (!group->HaveAllCisesDisconnected()) {
/* There is ASE streaming for some device. Continue streaming. */
LOG_WARN(
"Group member disconnected during streaming. Cis handle 0x%04x",
event->cis_conn_hdl);
return;
}
LOG_INFO("Lost all members from the group %d", group->group_id_);
group->cig.cises.clear();
RemoveCigForGroup(group);
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
group->SetTargetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
/* If there is no more ase to stream. Notify it is in IDLE. */
state_machine_callbacks_->StatusReportCb(group->group_id_,
GroupStreamStatus::IDLE);
return;
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
/* Intentional group disconnect has finished, but the last CIS in the
* event came after the ASE notification.
* If group is already suspended and all CIS are disconnected, we can
* report SUSPENDED state.
*/
if ((group->GetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) &&
group->HaveAllCisesDisconnected()) {
/* No more transition for group */
cancel_watchdog_if_needed(group->group_id_);
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::SUSPENDED);
return;
}
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED: {
/* Those two are used when closing the stream and CIS disconnection is
* expected */
if (!group->HaveAllCisesDisconnected()) {
LOG_DEBUG(
"Still waiting for all CISes being disconnected for group:%d",
group->group_id_);
return;
}
auto current_group_state = group->GetState();
LOG_INFO("group %d current state: %s, target state: %s",
group->group_id_,
bluetooth::common::ToString(current_group_state).c_str(),
bluetooth::common::ToString(target_state).c_str());
/* It might happen that controller notified about CIS disconnection
* later, after ASE state already changed.
* In such an event, there is need to notify upper layer about state
* from here.
*/
cancel_watchdog_if_needed(group->group_id_);
if (current_group_state == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
LOG_INFO(
"Cises disconnected for group %d, we are good in Idle state.",
group->group_id_);
ReleaseCisIds(group);
state_machine_callbacks_->StatusReportCb(group->group_id_,
GroupStreamStatus::IDLE);
} else if (current_group_state ==
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED) {
auto reconfig = group->IsPendingConfiguration();
LOG_INFO(
"Cises disconnected for group: %d, we are good in Configured "
"state, reconfig=%d.",
group->group_id_, reconfig);
/* This is Autonomous change if both, target and current state
* is CODEC_CONFIGURED
*/
if (target_state == current_group_state) {
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::CONFIGURED_AUTONOMOUS);
}
}
RemoveCigForGroup(group);
} break;
default:
break;
}
/* We should send Receiver Stop Ready when acting as a source */
if (ases_pair.source &&
ases_pair.source->state == AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING) {
std::vector<uint8_t> ids = {ases_pair.source->id};
std::vector<uint8_t> value;
le_audio::client_parser::ascs::PrepareAseCtpAudioReceiverStopReady(ids,
value);
WriteToControlPoint(leAudioDevice, value);
log_history_->AddLogHistory(kLogControlPointCmd, leAudioDevice->group_id_,
leAudioDevice->address_,
kLogAseStopReadyOp + "ASE_ID " +
std::to_string(ases_pair.source->id));
}
/* Tear down CIS's data paths within the group */
struct ase* ase = leAudioDevice->GetFirstActiveAseByCisAndDataPathState(
CisState::CONNECTED, DataPathState::CONFIGURED);
if (!ase) {
leAudioDevice = group->GetNextActiveDevice(leAudioDevice);
/* No more ASEs to disconnect their CISes */
if (!leAudioDevice) return;
ase = leAudioDevice->GetFirstActiveAse();
}
LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
if (ase->data_path_state == DataPathState::CONFIGURED) {
RemoveDataPathByCisHandle(leAudioDevice, ase->cis_conn_hdl);
}
}
private:
static constexpr uint64_t kStateTransitionTimeoutMs = 3500;
static constexpr char kStateTransitionTimeoutMsProp[] =
"persist.bluetooth.leaudio.device.set.state.timeoutms";
Callbacks* state_machine_callbacks_;
alarm_t* watchdog_;
LeAudioLogHistory* log_history_;
/* This callback is called on timeout during transition to target state */
void OnStateTransitionTimeout(int group_id) {
log_history_->AddLogHistory(kLogStateMachineTag, group_id,
RawAddress::kEmpty, "WATCHDOG FIRED");
state_machine_callbacks_->OnStateTransitionTimeout(group_id);
}
void SetTargetState(LeAudioDeviceGroup* group, AseState state) {
auto current_state = ToString(group->GetTargetState());
auto new_state = ToString(state);
LOG_DEBUG("Watchdog watch started for group=%d transition from %s to %s",
group->group_id_, current_state.c_str(), new_state.c_str());
group->SetTargetState(state);
/* Group should tie in time to get requested status */
uint64_t timeoutMs = kStateTransitionTimeoutMs;
timeoutMs =
osi_property_get_int32(kStateTransitionTimeoutMsProp, timeoutMs);
cancel_watchdog_if_needed(group->group_id_);
alarm_set_on_mloop(
watchdog_, timeoutMs,
[](void* data) {
if (instance) instance->OnStateTransitionTimeout(PTR_TO_INT(data));
},
INT_TO_PTR(group->group_id_));
log_history_->AddLogHistory(kLogStateMachineTag, group->group_id_,
RawAddress::kEmpty, "WATCHDOG STARTED");
}
void AddCisToStreamConfiguration(LeAudioDeviceGroup* group,
const struct ase* ase) {
group->stream_conf.codec_id = ase->codec_id;
auto cis_conn_hdl = ase->cis_conn_hdl;
auto& params = group->stream_conf.stream_params.get(ase->direction);
LOG_INFO("Adding cis handle 0x%04x (%s) to stream list", cis_conn_hdl,
ase->direction == le_audio::types::kLeAudioDirectionSink
? "sink"
: "source");
auto iter = std::find_if(
params.stream_locations.begin(), params.stream_locations.end(),
[cis_conn_hdl](auto& pair) { return cis_conn_hdl == pair.first; });
ASSERT_LOG(iter == params.stream_locations.end(),
"Stream is already there 0x%04x", cis_conn_hdl);
auto core_config = ase->codec_config.GetAsCoreCodecConfig();
params.num_of_devices++;
params.num_of_channels += core_config.GetChannelCountPerIsoStream();
if (!core_config.audio_channel_allocation.has_value()) {
LOG_WARN("ASE has invalid audio location");
}
auto ase_audio_channel_allocation =
core_config.audio_channel_allocation.value_or(0);
params.audio_channel_allocation |= ase_audio_channel_allocation;
params.stream_locations.emplace_back(
std::make_pair(ase->cis_conn_hdl, ase_audio_channel_allocation));
if (params.sample_frequency_hz == 0) {
params.sample_frequency_hz = core_config.GetSamplingFrequencyHz();
} else {
ASSERT_LOG(
params.sample_frequency_hz == core_config.GetSamplingFrequencyHz(),
"sample freq mismatch: %d!=%d", params.sample_frequency_hz,
core_config.GetSamplingFrequencyHz());
}
if (params.octets_per_codec_frame == 0) {
params.octets_per_codec_frame = *core_config.octets_per_codec_frame;
} else {
ASSERT_LOG(
params.octets_per_codec_frame == *core_config.octets_per_codec_frame,
"octets per frame mismatch: %d!=%d", params.octets_per_codec_frame,
*core_config.octets_per_codec_frame);
}
if (params.codec_frames_blocks_per_sdu == 0) {
params.codec_frames_blocks_per_sdu =
*core_config.codec_frames_blocks_per_sdu;
} else {
ASSERT_LOG(params.codec_frames_blocks_per_sdu ==
*core_config.codec_frames_blocks_per_sdu,
"codec_frames_blocks_per_sdu: %d!=%d",
params.codec_frames_blocks_per_sdu,
*core_config.codec_frames_blocks_per_sdu);
}
if (params.frame_duration_us == 0) {
params.frame_duration_us = core_config.GetFrameDurationUs();
} else {
ASSERT_LOG(params.frame_duration_us == core_config.GetFrameDurationUs(),
"frame_duration_us: %d!=%d", params.frame_duration_us,
core_config.GetFrameDurationUs());
}
LOG_INFO(
" Added %s Stream Configuration. CIS Connection Handle: %d"
", Audio Channel Allocation: %d"
", Number Of Devices: %d"
", Number Of Channels: %d",
(ase->direction == le_audio::types::kLeAudioDirectionSink ? "Sink"
: "Source"),
cis_conn_hdl, ase_audio_channel_allocation, params.num_of_devices,
params.num_of_channels);
/* Update CodecManager stream configuration */
state_machine_callbacks_->OnUpdatedCisConfiguration(group->group_id_,
ase->direction);
}
static bool isIntervalAndLatencyProperlySet(uint32_t sdu_interval_us,
uint16_t max_latency_ms) {
LOG_VERBOSE("sdu_interval_us: %d, max_latency_ms: %d", sdu_interval_us,
max_latency_ms);
if (sdu_interval_us == 0) {
return max_latency_ms == le_audio::types::kMaxTransportLatencyMin;
}
return ((1000 * max_latency_ms) >= sdu_interval_us);
}
void ApplyDsaParams(LeAudioDeviceGroup* group,
bluetooth::hci::iso_manager::cig_create_params& param) {
if (!IS_FLAG_ENABLED(leaudio_dynamic_spatial_audio)) {
return;
}
LOG_INFO("DSA mode selected: %d", (int)group->dsa_.mode);
group->dsa_.active = false;
/* Unidirectional streaming */
if (param.sdu_itv_stom == 0) {
LOG_INFO("Media streaming, apply DSA parameters");
switch (group->dsa_.mode) {
case DsaMode::ISO_HW:
case DsaMode::ISO_SW: {
auto& cis_cfgs = param.cis_cfgs;
auto it = cis_cfgs.begin();
for (auto dsa_modes : group->GetAllowedDsaModesList()) {
if (!dsa_modes.empty() && it != cis_cfgs.end()) {
if (std::find(dsa_modes.begin(), dsa_modes.end(),
group->dsa_.mode) != dsa_modes.end()) {
LOG_INFO("Device found with support for selected DsaMode");
group->dsa_.active = true;
/* Todo: Replace literal values */
param.sdu_itv_stom = 20000;
param.max_trans_lat_stom = 20;
it->max_sdu_size_stom = 15;
it->rtn_stom = 2;
it++;
}
}
}
} break;
case DsaMode::ACL:
/* Todo: Prioritize the ACL */
break;
case DsaMode::DISABLED:
default:
/* No need to change ISO parameters */
break;
}
} else {
LOG_DEBUG("Bidirection streaming, ignore DSA mode");
}
}
bool CigCreate(LeAudioDeviceGroup* group) {
uint32_t sdu_interval_mtos, sdu_interval_stom;
uint16_t max_trans_lat_mtos, max_trans_lat_stom;
uint8_t packing, framing, sca;
std::vector<EXT_CIS_CFG> cis_cfgs;
LOG_DEBUG("Group: %p, id: %d cig state: %s", group, group->group_id_,
ToString(group->cig.GetState()).c_str());
if (group->cig.GetState() != CigState::NONE) {
LOG_WARN(" Group %p, id: %d has invalid cig state: %s ", group,
group->group_id_, ToString(group->cig.GetState()).c_str());
return false;
}
sdu_interval_mtos =
group->GetSduInterval(le_audio::types::kLeAudioDirectionSink);
sdu_interval_stom =
group->GetSduInterval(le_audio::types::kLeAudioDirectionSource);
sca = group->GetSCA();
packing = group->GetPacking();
framing = group->GetFraming();
max_trans_lat_mtos = group->GetMaxTransportLatencyMtos();
max_trans_lat_stom = group->GetMaxTransportLatencyStom();
uint16_t max_sdu_size_mtos = 0;
uint16_t max_sdu_size_stom = 0;
uint8_t phy_mtos =
group->GetPhyBitmask(le_audio::types::kLeAudioDirectionSink);
uint8_t phy_stom =
group->GetPhyBitmask(le_audio::types::kLeAudioDirectionSource);
if (!isIntervalAndLatencyProperlySet(sdu_interval_mtos,
max_trans_lat_mtos) ||
!isIntervalAndLatencyProperlySet(sdu_interval_stom,
max_trans_lat_stom)) {
LOG_ERROR("Latency and interval not properly set");
group->PrintDebugState();
return false;
}
// Use 1M Phy for the ACK packet from remote device to phone for better
// sensitivity
if (group->asymmetric_phy_for_unidirectional_cis_supported &&
sdu_interval_stom == 0 &&
(phy_stom & bluetooth::hci::kIsoCigPhy1M) != 0) {
LOG_INFO("Use asymmetric PHY for unidirectional CIS");
phy_stom = bluetooth::hci::kIsoCigPhy1M;
}
uint8_t rtn_mtos = 0;
uint8_t rtn_stom = 0;
/* Currently assumed Sink/Source configuration is same across cis types.
* If a cis in cises_ is currently associated with active device/ASE(s),
* use the Sink/Source configuration for the same.
* If a cis in cises_ is not currently associated with active device/ASE(s),
* use the Sink/Source configuration for the cis in cises_
* associated with a active device/ASE(s). When the same cis is associated
* later, with active device/ASE(s), check if current configuration is
* supported or not, if not, reconfigure CIG.
*/
for (struct le_audio::types::cis& cis : group->cig.cises) {
uint16_t max_sdu_size_mtos_temp =
group->GetMaxSduSize(le_audio::types::kLeAudioDirectionSink, cis.id);
uint16_t max_sdu_size_stom_temp = group->GetMaxSduSize(
le_audio::types::kLeAudioDirectionSource, cis.id);
uint8_t rtn_mtos_temp =
group->GetRtn(le_audio::types::kLeAudioDirectionSink, cis.id);
uint8_t rtn_stom_temp =
group->GetRtn(le_audio::types::kLeAudioDirectionSource, cis.id);
max_sdu_size_mtos =
max_sdu_size_mtos_temp ? max_sdu_size_mtos_temp : max_sdu_size_mtos;
max_sdu_size_stom =
max_sdu_size_stom_temp ? max_sdu_size_stom_temp : max_sdu_size_stom;
rtn_mtos = rtn_mtos_temp ? rtn_mtos_temp : rtn_mtos;
rtn_stom = rtn_stom_temp ? rtn_stom_temp : rtn_stom;
}
for (struct le_audio::types::cis& cis : group->cig.cises) {
EXT_CIS_CFG cis_cfg = {};
cis_cfg.cis_id = cis.id;
cis_cfg.phy_mtos = phy_mtos;
cis_cfg.phy_stom = phy_stom;
if (cis.type == le_audio::types::CisType::CIS_TYPE_BIDIRECTIONAL) {
cis_cfg.max_sdu_size_mtos = max_sdu_size_mtos;
cis_cfg.rtn_mtos = rtn_mtos;
cis_cfg.max_sdu_size_stom = max_sdu_size_stom;
cis_cfg.rtn_stom = rtn_stom;
cis_cfgs.push_back(cis_cfg);
} else if (cis.type ==
le_audio::types::CisType::CIS_TYPE_UNIDIRECTIONAL_SINK) {
cis_cfg.max_sdu_size_mtos = max_sdu_size_mtos;
cis_cfg.rtn_mtos = rtn_mtos;
cis_cfg.max_sdu_size_stom = 0;
cis_cfg.rtn_stom = 0;
cis_cfgs.push_back(cis_cfg);
} else {
cis_cfg.max_sdu_size_mtos = 0;
cis_cfg.rtn_mtos = 0;
cis_cfg.max_sdu_size_stom = max_sdu_size_stom;
cis_cfg.rtn_stom = rtn_stom;
cis_cfgs.push_back(cis_cfg);
}
}
if ((sdu_interval_mtos == 0 && sdu_interval_stom == 0) ||
(max_trans_lat_mtos == le_audio::types::kMaxTransportLatencyMin &&
max_trans_lat_stom == le_audio::types::kMaxTransportLatencyMin) ||
(max_sdu_size_mtos == 0 && max_sdu_size_stom == 0)) {
LOG_ERROR(" Trying to create invalid group");
group->PrintDebugState();
return false;
}
bluetooth::hci::iso_manager::cig_create_params param = {
.sdu_itv_mtos = sdu_interval_mtos,
.sdu_itv_stom = sdu_interval_stom,
.sca = sca,
.packing = packing,
.framing = framing,
.max_trans_lat_stom = max_trans_lat_stom,
.max_trans_lat_mtos = max_trans_lat_mtos,
.cis_cfgs = std::move(cis_cfgs),
};
ApplyDsaParams(group, param);
log_history_->AddLogHistory(
kLogStateMachineTag, group->group_id_, RawAddress::kEmpty,
kLogCigCreateOp + "#CIS: " + std::to_string(param.cis_cfgs.size()));
group->cig.SetState(CigState::CREATING);
IsoManager::GetInstance()->CreateCig(group->group_id_, std::move(param));
LOG_DEBUG("Group: %p, id: %d cig state: %s", group, group->group_id_,
ToString(group->cig.GetState()).c_str());
return true;
}
static bool CisCreateForDevice(LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice) {
std::vector<EXT_CIS_CREATE_CFG> conn_pairs;
struct ase* ase = leAudioDevice->GetFirstActiveAse();
/* Make sure CIG is there */
if (group->cig.GetState() != CigState::CREATED) {
LOG_ERROR("CIG is not created for group_id %d ", group->group_id_);
group->PrintDebugState();
return false;
}
std::stringstream extra_stream;
do {
/* First in ase pair is Sink, second Source */
auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(ase->cis_conn_hdl);
/* Already in pending state - bi-directional CIS or seconde CIS to same
* device */
if (ase->cis_state == CisState::CONNECTING ||
ase->cis_state == CisState::CONNECTED)
continue;
if (ases_pair.sink) ases_pair.sink->cis_state = CisState::CONNECTING;
if (ases_pair.source) ases_pair.source->cis_state = CisState::CONNECTING;
uint16_t acl_handle =
BTM_GetHCIConnHandle(leAudioDevice->address_, BT_TRANSPORT_LE);
conn_pairs.push_back({.cis_conn_handle = ase->cis_conn_hdl,
.acl_conn_handle = acl_handle});
LOG_INFO(" cis handle: 0x%04x, acl handle: 0x%04x", ase->cis_conn_hdl,
acl_handle);
extra_stream << "cis_h:" << loghex(ase->cis_conn_hdl)
<< " acl_h:" << loghex(acl_handle) << ";;";
} while ((ase = leAudioDevice->GetNextActiveAse(ase)));
LeAudioLogHistory::Get()->AddLogHistory(
kLogStateMachineTag, leAudioDevice->group_id_, RawAddress::kEmpty,
kLogCisCreateOp + "#CIS: " + std::to_string(conn_pairs.size()),
extra_stream.str());
IsoManager::GetInstance()->EstablishCis(
{.conn_pairs = std::move(conn_pairs)});
return true;
}
static bool CisCreate(LeAudioDeviceGroup* group) {
LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
struct ase* ase;
std::vector<EXT_CIS_CREATE_CFG> conn_pairs;
LOG_ASSERT(leAudioDevice)
<< __func__ << " Shouldn't be called without an active device.";
/* Make sure CIG is there */
if (group->cig.GetState() != CigState::CREATED) {
LOG_ERROR("CIG is not created for group_id %d ", group->group_id_);
group->PrintDebugState();
return false;
}
do {
ase = leAudioDevice->GetFirstActiveAse();
LOG_ASSERT(ase) << __func__
<< " shouldn't be called without an active ASE";
do {
/* First is ase pair is Sink, second Source */
auto ases_pair = leAudioDevice->GetAsesByCisConnHdl(ase->cis_conn_hdl);
/* Already in pending state - bi-directional CIS */
if (ase->cis_state == CisState::CONNECTING) continue;
if (ases_pair.sink) ases_pair.sink->cis_state = CisState::CONNECTING;
if (ases_pair.source)
ases_pair.source->cis_state = CisState::CONNECTING;
uint16_t acl_handle =
BTM_GetHCIConnHandle(leAudioDevice->address_, BT_TRANSPORT_LE);
conn_pairs.push_back({.cis_conn_handle = ase->cis_conn_hdl,
.acl_conn_handle = acl_handle});
DLOG(INFO) << __func__ << " cis handle: " << +ase->cis_conn_hdl
<< " acl handle : " << loghex(+acl_handle);
} while ((ase = leAudioDevice->GetNextActiveAse(ase)));
} while ((leAudioDevice = group->GetNextActiveDevice(leAudioDevice)));
IsoManager::GetInstance()->EstablishCis(
{.conn_pairs = std::move(conn_pairs)});
return true;
}
static void PrepareDataPath(int group_id, struct ase* ase) {
bluetooth::hci::iso_manager::iso_data_path_params param = {
.data_path_dir =
ase->direction == le_audio::types::kLeAudioDirectionSink
? bluetooth::hci::iso_manager::kIsoDataPathDirectionIn
: bluetooth::hci::iso_manager::kIsoDataPathDirectionOut,
.data_path_id = ase->data_path_id,
.codec_id_format = ase->is_codec_in_controller
? ase->codec_id.coding_format
: bluetooth::hci::kIsoCodingFormatTransparent,
.codec_id_company = ase->codec_id.vendor_company_id,
.codec_id_vendor = ase->codec_id.vendor_codec_id,
.controller_delay = 0x00000000,
.codec_conf = std::vector<uint8_t>(),
};
LeAudioLogHistory::Get()->AddLogHistory(
kLogStateMachineTag, group_id, RawAddress::kEmpty,
kLogSetDataPathOp + "cis_h:" + loghex(ase->cis_conn_hdl),
"direction: " + loghex(param.data_path_dir));
ase->data_path_state = DataPathState::CONFIGURING;
IsoManager::GetInstance()->SetupIsoDataPath(ase->cis_conn_hdl,
std::move(param));
}
static void ReleaseDataPath(LeAudioDeviceGroup* group) {
LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
LOG_ASSERT(leAudioDevice)
<< __func__ << " Shouldn't be called without an active device.";
auto ase = leAudioDevice->GetFirstActiveAseByCisAndDataPathState(
CisState::CONNECTED, DataPathState::CONFIGURED);
LOG_ASSERT(ase) << __func__
<< " Shouldn't be called without an active ASE.";
RemoveDataPathByCisHandle(leAudioDevice, ase->cis_conn_hdl);
}
void SetAseState(LeAudioDevice* leAudioDevice, struct ase* ase,
AseState state) {
LOG_INFO("%s, ase_id: %d, %s -> %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id,
ToString(ase->state).c_str(), ToString(state).c_str());
log_history_->AddLogHistory(
kLogStateMachineTag, leAudioDevice->group_id_, leAudioDevice->address_,
"ASE_ID " + std::to_string(ase->id) + ": " + kLogStateChangedOp,
ToString(ase->state) + "->" + ToString(state));
ase->state = state;
}
void AseStateMachineProcessIdle(
struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
switch (ase->state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING: {
SetAseState(leAudioDevice, ase, AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
ase->active = false;
ase->configured_for_context_type =
le_audio::types::LeAudioContextType::UNINITIALIZED;
if (!leAudioDevice->HaveAllActiveAsesSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_IDLE)) {
/* More ASEs notification from this device has to come for this group
*/
LOG_DEBUG("Wait for more ASE to configure for device %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
return;
}
/* Before continue with release, make sure this is what is requested.
* If not (e.g. only single device got disconnected), stop here
*/
if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
LOG_DEBUG("Autonomus change of stated for device %s, ase id: %d",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
return;
}
if (!group->HaveAllActiveDevicesAsesTheSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_IDLE)) {
LOG_DEBUG("Waiting for more devices to get into idle state");
return;
}
/* Last node is in releasing state*/
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
group->PrintDebugState();
/* If all CISes are disconnected, notify upper layer about IDLE state,
* otherwise wait for */
if (!group->HaveAllCisesDisconnected()) {
LOG_WARN(
"Not all CISes removed before going to IDLE for group %d, "
"waiting...",
group->group_id_);
group->PrintDebugState();
return;
}
cancel_watchdog_if_needed(group->group_id_);
ReleaseCisIds(group);
state_machine_callbacks_->StatusReportCb(group->group_id_,
GroupStreamStatus::IDLE);
break;
}
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
case AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING:
LOG_ERROR(
"Ignore invalid attempt of state transition from %s to %s, %s, "
"ase_id: %d",
ToString(ase->state).c_str(),
ToString(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE).c_str(),
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
group->PrintDebugState();
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
LOG_ERROR(
"Invalid state transition from %s to %s, %s, ase_id: "
"%d. Stopping the stream.",
ToString(ase->state).c_str(),
ToString(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE).c_str(),
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
group->PrintDebugState();
StopStream(group);
break;
}
}
void PrepareAndSendQoSToTheGroup(LeAudioDeviceGroup* group) {
LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
if (!leAudioDevice) {
LOG_ERROR("No active device for the group");
group->PrintDebugState();
ClearGroup(group, true);
return;
}
for (; leAudioDevice;
leAudioDevice = group->GetNextActiveDevice(leAudioDevice)) {
PrepareAndSendConfigQos(group, leAudioDevice);
}
}
bool PrepareAndSendCodecConfigToTheGroup(LeAudioDeviceGroup* group) {
LOG_INFO("group_id: %d", group->group_id_);
auto leAudioDevice = group->GetFirstActiveDevice();
if (!leAudioDevice) {
LOG_ERROR("No active device for the group");
return false;
}
for (; leAudioDevice;
leAudioDevice = group->GetNextActiveDevice(leAudioDevice)) {
PrepareAndSendCodecConfigure(group, leAudioDevice);
}
return true;
}
void PrepareAndSendCodecConfigure(LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice) {
struct le_audio::client_parser::ascs::ctp_codec_conf conf;
std::vector<struct le_audio::client_parser::ascs::ctp_codec_conf> confs;
struct ase* ase;
std::stringstream msg_stream;
std::stringstream extra_stream;
if (!group->cig.AssignCisIds(leAudioDevice)) {
LOG_ERROR(" unable to assign CIS IDs");
StopStream(group);
return;
}
if (group->cig.GetState() == CigState::CREATED)
group->AssignCisConnHandlesToAses(leAudioDevice);
msg_stream << kLogAseConfigOp;
ase = leAudioDevice->GetFirstActiveAse();
ASSERT_LOG(ase, "shouldn't be called without an active ASE");
for (; ase != nullptr; ase = leAudioDevice->GetNextActiveAse(ase)) {
LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id,
ase->cis_id, ToString(ase->state).c_str());
conf.ase_id = ase->id;
conf.target_latency = ase->target_latency;
conf.target_phy = group->GetTargetPhy(ase->direction);
conf.codec_id = ase->codec_id;
conf.codec_config = ase->codec_config;
confs.push_back(conf);
msg_stream << "ASE_ID " << +conf.ase_id << ",";
if (ase->direction == le_audio::types::kLeAudioDirectionSink) {
extra_stream << "snk,";
} else {
extra_stream << "src,";
}
extra_stream << +conf.codec_id.coding_format << ","
<< +conf.target_latency << ";;";
}
std::vector<uint8_t> value;
le_audio::client_parser::ascs::PrepareAseCtpCodecConfig(confs, value);
WriteToControlPoint(leAudioDevice, value);
log_history_->AddLogHistory(kLogControlPointCmd, group->group_id_,
leAudioDevice->address_, msg_stream.str(),
extra_stream.str());
}
void AseStateMachineProcessCodecConfigured(
struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
uint8_t* data, uint16_t len, LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice) {
if (!group) {
LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
return;
}
/* ase contain current ASE state. New state is in "arh" */
switch (ase->state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE: {
struct le_audio::client_parser::ascs::ase_codec_configured_state_params
rsp;
/* Cache codec configured status values for further
* configuration/reconfiguration
*/
if (!ParseAseStatusCodecConfiguredStateParams(rsp, len, data)) {
StopStream(group);
return;
}
uint16_t cig_curr_max_trans_lat_mtos =
group->GetMaxTransportLatencyMtos();
uint16_t cig_curr_max_trans_lat_stom =
group->GetMaxTransportLatencyStom();
if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
/* We are here because of the reconnection of the single device.
* Reconfigure CIG if current CIG supported Max Transport Latency for
* a direction, cannot be supported by the newly connected member
* device's ASE for the direction.
*/
if ((ase->direction == le_audio::types::kLeAudioDirectionSink &&
cig_curr_max_trans_lat_mtos > rsp.max_transport_latency) ||
(ase->direction == le_audio::types::kLeAudioDirectionSource &&
cig_curr_max_trans_lat_stom > rsp.max_transport_latency)) {
group->SetPendingConfiguration();
StopStream(group);
return;
}
}
ase->framing = rsp.framing;
ase->preferred_phy = rsp.preferred_phy;
/* Validate and update QoS settings to be consistent */
if ((!ase->max_transport_latency ||
ase->max_transport_latency > rsp.max_transport_latency) ||
!ase->retrans_nb) {
ase->max_transport_latency = rsp.max_transport_latency;
ase->retrans_nb = rsp.preferred_retrans_nb;
LOG_INFO(
" Using server preferred QoS settings. Max Transport Latency: %d"
", Retransmission Number: %d",
+ase->max_transport_latency, ase->retrans_nb);
}
ase->pres_delay_min = rsp.pres_delay_min;
ase->pres_delay_max = rsp.pres_delay_max;
ase->preferred_pres_delay_min = rsp.preferred_pres_delay_min;
ase->preferred_pres_delay_max = rsp.preferred_pres_delay_max;
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
if (group->GetTargetState() == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
/* This is autonomus change of the remote device */
LOG_DEBUG("Autonomus change for device %s, ase id %d. Just store it.",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
/* Since at least one ASE is in configured state, we should admit
* group is configured state */
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
return;
}
if (leAudioDevice->HaveAnyUnconfiguredAses()) {
/* More ASEs notification from this device has to come for this group
*/
LOG_DEBUG("More Ases to be configured for the device %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
return;
}
if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
/* We are here because of the reconnection of the single device. */
/* Make sure that device is ready to be configured as we could also
* get here triggered by the remote device. If device is not connected
* yet, we should wait for the stack to trigger adding device to the
* stream */
if (leAudioDevice->GetConnectionState() ==
le_audio::DeviceConnectState::CONNECTED) {
PrepareAndSendConfigQos(group, leAudioDevice);
} else {
LOG_DEBUG(
"Device %s initiated configured state but it is not yet ready "
"to be configured",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
}
return;
}
/* Configure ASEs for next device in group */
if (group->HaveAnyActiveDeviceInUnconfiguredState()) {
LOG_DEBUG("Waiting for all the ASES in the Configured state");
return;
}
/* Last node configured, process group to codec configured state */
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
if (group->GetTargetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
if (!CigCreate(group)) {
LOG_ERROR("Could not create CIG. Stop the stream for group %d",
group->group_id_);
StopStream(group);
}
return;
}
if (group->GetTargetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED &&
group->IsPendingConfiguration()) {
LOG_INFO(" Configured state completed ");
/* If all CISes are disconnected, notify upper layer about IDLE
* state, otherwise wait for */
if (!group->HaveAllCisesDisconnected()) {
LOG_WARN(
"Not all CISes removed before going to CONFIGURED for group "
"%d, "
"waiting...",
group->group_id_);
group->PrintDebugState();
return;
}
group->ClearPendingConfiguration();
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::CONFIGURED_BY_USER);
/* No more transition for group */
cancel_watchdog_if_needed(group->group_id_);
return;
}
LOG_ERROR(", invalid state transition, from: %s to %s",
ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
StopStream(group);
break;
}
case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED: {
/* Received Configured in Configured state. This could be done
* autonomously because of the reconfiguration done by us
*/
struct le_audio::client_parser::ascs::ase_codec_configured_state_params
rsp;
/* Cache codec configured status values for further
* configuration/reconfiguration
*/
if (!ParseAseStatusCodecConfiguredStateParams(rsp, len, data)) {
StopStream(group);
return;
}
ase->framing = rsp.framing;
ase->preferred_phy = rsp.preferred_phy;
/* Validate and update QoS settings to be consistent */
if ((!ase->max_transport_latency ||
ase->max_transport_latency > rsp.max_transport_latency) ||
!ase->retrans_nb) {
ase->max_transport_latency = rsp.max_transport_latency;
ase->retrans_nb = rsp.preferred_retrans_nb;
LOG(INFO) << __func__ << " Using server preferred QoS settings."
<< " Max Transport Latency: " << +ase->max_transport_latency
<< ", Retransmission Number: " << +ase->retrans_nb;
}
ase->pres_delay_min = rsp.pres_delay_min;
ase->pres_delay_max = rsp.pres_delay_max;
ase->preferred_pres_delay_min = rsp.preferred_pres_delay_min;
ase->preferred_pres_delay_max = rsp.preferred_pres_delay_max;
/* This may be a notification from a re-configured ASE */
ase->reconfigure = false;
if (leAudioDevice->HaveAnyUnconfiguredAses()) {
/* Waiting for others to be reconfigured */
return;
}
if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
/* We are here because of the reconnection of the single device. */
/* Make sure that device is ready to be configured as we could also
* get here triggered by the remote device. If device is not connected
* yet, we should wait for the stack to trigger adding device to the
* stream */
if (leAudioDevice->GetConnectionState() ==
le_audio::DeviceConnectState::CONNECTED) {
PrepareAndSendConfigQos(group, leAudioDevice);
} else {
LOG_DEBUG(
"Device %s initiated configured state but it is not yet ready "
"to be configured",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
}
return;
}
if (group->HaveAnyActiveDeviceInUnconfiguredState()) {
LOG_DEBUG(
"Waiting for all the devices to be configured for group id %d",
group->group_id_);
return;
}
/* Last node configured, process group to codec configured state */
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
if (group->GetTargetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
if (!CigCreate(group)) {
LOG_ERROR("Could not create CIG. Stop the stream for group %d",
group->group_id_);
StopStream(group);
}
return;
}
if (group->GetTargetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED &&
group->IsPendingConfiguration()) {
LOG_INFO(" Configured state completed ");
group->ClearPendingConfiguration();
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::CONFIGURED_BY_USER);
/* No more transition for group */
cancel_watchdog_if_needed(group->group_id_);
return;
}
LOG_INFO("Autonomous change, from: %s to %s",
ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
break;
}
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
group->PrintDebugState();
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING:
LOG_ERROR(
"Ignore invalid attempt of state transition from %s to %s, %s, "
"ase_id: %d",
ToString(ase->state).c_str(),
ToString(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED).c_str(),
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
group->PrintDebugState();
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING:
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
ase->active = false;
if (!leAudioDevice->HaveAllActiveAsesSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED)) {
/* More ASEs notification from this device has to come for this group
*/
LOG_DEBUG("Wait for more ASE to configure for device %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
return;
}
/* Before continue with release, make sure this is what is requested.
* If not (e.g. only single device got disconnected), stop here
*/
if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
LOG_DEBUG("Autonomus change of stated for device %s, ase id: %d",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
return;
}
{
auto activeDevice = group->GetFirstActiveDevice();
if (activeDevice) {
LOG_DEBUG(
"There is at least one active device %s, wait to become "
"inactive",
ADDRESS_TO_LOGGABLE_CSTR(activeDevice->address_));
return;
}
}
/* Last node is in releasing state*/
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
/* Remote device has cache and keep staying in configured state after
* release. Therefore, we assume this is a target state requested by
* remote device.
*/
group->SetTargetState(group->GetState());
if (!group->HaveAllCisesDisconnected()) {
LOG_WARN(
"Not all CISes removed before going to IDLE for group %d, "
"waiting...",
group->group_id_);
group->PrintDebugState();
return;
}
cancel_watchdog_if_needed(group->group_id_);
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::CONFIGURED_AUTONOMOUS);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
LOG_ERROR(
"Invalid state transition from %s to %s, %s, ase_id: %d. Stopping "
"the stream",
ToString(ase->state).c_str(),
ToString(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED).c_str(),
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
group->PrintDebugState();
StopStream(group);
break;
}
}
void AseStateMachineProcessQosConfigured(
struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
if (!group) {
LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
return;
}
switch (ase->state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED: {
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
if (!leAudioDevice->HaveAllActiveAsesSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED)) {
/* More ASEs notification from this device has to come for this group
*/
return;
}
if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
/* We are here because of the reconnection of the single device. */
PrepareAndSendEnable(leAudioDevice);
return;
}
if (!group->HaveAllActiveDevicesAsesTheSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED)) {
LOG_DEBUG("Waiting for all the devices to be in QoS state");
return;
}
PrepareAndSendEnableToTheGroup(group);
break;
}
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
if (ase->direction == le_audio::types::kLeAudioDirectionSource) {
/* Source ASE cannot go from Streaming to QoS Configured state */
LOG(ERROR) << __func__ << ", invalid state transition, from: "
<< static_cast<int>(ase->state) << ", to: "
<< static_cast<int>(
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
StopStream(group);
return;
}
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
/* Remote may autonomously bring ASEs to QoS configured state */
if (group->GetTargetState() !=
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) {
ProcessAutonomousDisable(leAudioDevice, ase);
}
/* Process the Disable Transition of the rest of group members if no
* more ASE notifications has to come from this device. */
if (leAudioDevice->IsReadyToSuspendStream()) ProcessGroupDisable(group);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING: {
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
/* More ASEs notification from this device has to come for this group */
if (!group->HaveAllActiveDevicesAsesTheSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED))
return;
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
if (!group->HaveAllCisesDisconnected()) return;
if (group->GetTargetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) {
/* No more transition for group */
cancel_watchdog_if_needed(group->group_id_);
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::SUSPENDED);
} else {
LOG_ERROR(", invalid state transition, from: %s, to: %s",
ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
StopStream(group);
return;
}
break;
}
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
LOG_INFO(
"Unexpected state transition from %s to %s, %s, ase_id: %d",
ToString(ase->state).c_str(),
ToString(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED).c_str(),
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
group->PrintDebugState();
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
case AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING:
// Do nothing here, just print an error message
LOG_ERROR(
"Ignore invalid attempt of state transition from %s to %s, %s, "
"ase_id: %d",
ToString(ase->state).c_str(),
ToString(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED).c_str(),
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
group->PrintDebugState();
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
LOG_ERROR(
"Invalid state transition from %s to %s, %s, ase_id: "
"%d. Stopping the stream.",
ToString(ase->state).c_str(),
ToString(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED).c_str(),
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
StopStream(group);
break;
}
}
void ClearGroup(LeAudioDeviceGroup* group, bool report_idle_state) {
LOG_DEBUG("group_id: %d", group->group_id_);
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
group->SetTargetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
/* Clear group pending status */
group->ClearPendingAvailableContextsChange();
group->ClearPendingConfiguration();
cancel_watchdog_if_needed(group->group_id_);
ReleaseCisIds(group);
RemoveCigForGroup(group);
if (report_idle_state) {
state_machine_callbacks_->StatusReportCb(group->group_id_,
GroupStreamStatus::IDLE);
}
}
void PrepareAndSendEnableToTheGroup(LeAudioDeviceGroup* group) {
LOG_INFO("group_id: %d", group->group_id_);
auto leAudioDevice = group->GetFirstActiveDevice();
if (!leAudioDevice) {
LOG_ERROR("No active device for the group");
group->PrintDebugState();
ClearGroup(group, true);
return;
}
for (; leAudioDevice;
leAudioDevice = group->GetNextActiveDevice(leAudioDevice)) {
PrepareAndSendEnable(leAudioDevice);
}
}
void PrepareAndSendEnable(LeAudioDevice* leAudioDevice) {
struct le_audio::client_parser::ascs::ctp_enable conf;
std::vector<struct le_audio::client_parser::ascs::ctp_enable> confs;
std::vector<uint8_t> value;
struct ase* ase;
std::stringstream msg_stream;
std::stringstream extra_stream;
msg_stream << kLogAseEnableOp;
ase = leAudioDevice->GetFirstActiveAse();
LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
do {
LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id,
ase->cis_id, ToString(ase->state).c_str());
conf.ase_id = ase->id;
conf.metadata = ase->metadata;
confs.push_back(conf);
/* Below is just for log history */
msg_stream << "ASE_ID " << +ase->id << ",";
extra_stream << "meta: "
<< base::HexEncode(conf.metadata.data(),
conf.metadata.size())
<< ";;";
} while ((ase = leAudioDevice->GetNextActiveAse(ase)));
le_audio::client_parser::ascs::PrepareAseCtpEnable(confs, value);
WriteToControlPoint(leAudioDevice, value);
LOG_INFO("group_id: %d, %s", leAudioDevice->group_id_,
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
log_history_->AddLogHistory(kLogControlPointCmd, leAudioDevice->group_id_,
leAudioDevice->address_, msg_stream.str(),
extra_stream.str());
}
GroupStreamStatus PrepareAndSendDisableToTheGroup(LeAudioDeviceGroup* group) {
LOG_INFO("grop_id: %d", group->group_id_);
auto leAudioDevice = group->GetFirstActiveDevice();
if (!leAudioDevice) {
LOG_ERROR("No active device for the group");
group->PrintDebugState();
ClearGroup(group, false);
return GroupStreamStatus::IDLE;
}
for (; leAudioDevice;
leAudioDevice = group->GetNextActiveDevice(leAudioDevice)) {
PrepareAndSendDisable(leAudioDevice);
}
return GroupStreamStatus::SUSPENDING;
}
void PrepareAndSendDisable(LeAudioDevice* leAudioDevice) {
ase* ase = leAudioDevice->GetFirstActiveAse();
LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
std::stringstream msg_stream;
msg_stream << kLogAseDisableOp;
std::vector<uint8_t> ids;
do {
LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id,
ase->cis_id, ToString(ase->state).c_str());
ids.push_back(ase->id);
msg_stream << "ASE_ID " << +ase->id << ", ";
} while ((ase = leAudioDevice->GetNextActiveAse(ase)));
LOG_INFO("group_id: %d, %s", leAudioDevice->group_id_,
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
std::vector<uint8_t> value;
le_audio::client_parser::ascs::PrepareAseCtpDisable(ids, value);
WriteToControlPoint(leAudioDevice, value);
log_history_->AddLogHistory(kLogControlPointCmd, leAudioDevice->group_id_,
leAudioDevice->address_, msg_stream.str());
}
GroupStreamStatus PrepareAndSendReleaseToTheGroup(LeAudioDeviceGroup* group) {
LOG_INFO("group_id: %d", group->group_id_);
LeAudioDevice* leAudioDevice = group->GetFirstActiveDevice();
if (!leAudioDevice) {
LOG_ERROR("No active device for the group");
group->PrintDebugState();
ClearGroup(group, false);
return GroupStreamStatus::IDLE;
}
for (; leAudioDevice;
leAudioDevice = group->GetNextActiveDevice(leAudioDevice)) {
PrepareAndSendRelease(leAudioDevice);
}
return GroupStreamStatus::RELEASING;
}
void PrepareAndSendRelease(LeAudioDevice* leAudioDevice) {
ase* ase = leAudioDevice->GetFirstActiveAse();
LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
std::vector<uint8_t> ids;
std::stringstream stream;
stream << kLogAseReleaseOp;
do {
LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id,
ase->cis_id, ToString(ase->state).c_str());
ids.push_back(ase->id);
stream << "ASE_ID " << +ase->id << ",";
} while ((ase = leAudioDevice->GetNextActiveAse(ase)));
std::vector<uint8_t> value;
le_audio::client_parser::ascs::PrepareAseCtpRelease(ids, value);
WriteToControlPoint(leAudioDevice, value);
LOG_INFO("group_id: %d, %s", leAudioDevice->group_id_,
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
log_history_->AddLogHistory(kLogControlPointCmd, leAudioDevice->group_id_,
leAudioDevice->address_, stream.str());
}
void PrepareAndSendConfigQos(LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice) {
std::vector<struct le_audio::client_parser::ascs::ctp_qos_conf> confs;
bool validate_transport_latency = false;
bool validate_max_sdu_size = false;
std::stringstream msg_stream;
msg_stream << kLogAseQoSConfigOp;
std::stringstream extra_stream;
for (struct ase* ase = leAudioDevice->GetFirstActiveAse(); ase != nullptr;
ase = leAudioDevice->GetNextActiveAse(ase)) {
LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id,
ase->cis_id, ToString(ase->state).c_str());
/* TODO: Configure first ASE qos according to context type */
struct le_audio::client_parser::ascs::ctp_qos_conf conf;
conf.ase_id = ase->id;
conf.cig = group->group_id_;
conf.cis = ase->cis_id;
conf.framing = group->GetFraming();
conf.phy = group->GetPhyBitmask(ase->direction);
conf.max_sdu = ase->max_sdu_size;
conf.retrans_nb = ase->retrans_nb;
if (!group->GetPresentationDelay(&conf.pres_delay, ase->direction)) {
LOG_ERROR("inconsistent presentation delay for group");
group->PrintDebugState();
StopStream(group);
return;
}
conf.sdu_interval = group->GetSduInterval(ase->direction);
if (!conf.sdu_interval) {
LOG_ERROR("unsupported SDU interval for group");
group->PrintDebugState();
StopStream(group);
return;
}
msg_stream << "ASE " << +conf.ase_id << ",";
if (ase->direction == le_audio::types::kLeAudioDirectionSink) {
conf.max_transport_latency = group->GetMaxTransportLatencyMtos();
extra_stream << "snk,";
} else {
conf.max_transport_latency = group->GetMaxTransportLatencyStom();
extra_stream << "src,";
}
if (conf.max_transport_latency >
le_audio::types::kMaxTransportLatencyMin) {
validate_transport_latency = true;
}
if (conf.max_sdu > 0) {
validate_max_sdu_size = true;
}
confs.push_back(conf);
// dir...cis_id,sdu,lat,rtn,phy,frm;;
extra_stream << +conf.cis << "," << +conf.max_sdu << ","
<< +conf.max_transport_latency << "," << +conf.retrans_nb
<< "," << +conf.phy << "," << +conf.framing << ";;";
}
if (confs.size() == 0 || !validate_transport_latency ||
!validate_max_sdu_size) {
LOG_ERROR("Invalid configuration or latency or sdu size");
group->PrintDebugState();
StopStream(group);
return;
}
std::vector<uint8_t> value;
le_audio::client_parser::ascs::PrepareAseCtpConfigQos(confs, value);
WriteToControlPoint(leAudioDevice, value);
LOG_INFO("group_id: %d, %s", leAudioDevice->group_id_,
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
log_history_->AddLogHistory(kLogControlPointCmd, group->group_id_,
leAudioDevice->address_, msg_stream.str(),
extra_stream.str());
}
void PrepareAndSendUpdateMetadata(
LeAudioDevice* leAudioDevice,
const BidirectionalPair<AudioContexts>& context_types,
const BidirectionalPair<std::vector<uint8_t>>& ccid_lists) {
std::vector<struct le_audio::client_parser::ascs::ctp_update_metadata>
confs;
std::stringstream msg_stream;
msg_stream << kLogAseUpdateMetadataOp;
std::stringstream extra_stream;
if (!leAudioDevice->IsMetadataChanged(context_types, ccid_lists)) return;
/* Request server to update ASEs with new metadata */
for (struct ase* ase = leAudioDevice->GetFirstActiveAse(); ase != nullptr;
ase = leAudioDevice->GetNextActiveAse(ase)) {
LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id,
ase->cis_id, ToString(ase->state).c_str());
if (ase->state != AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING &&
ase->state != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
/* This might happen when update metadata happens on late connect */
LOG_DEBUG(
"Metadata for ase_id %d cannot be updated due to invalid ase state "
"- see log above",
ase->id);
continue;
}
msg_stream << "ASE_ID " << +ase->id << ",";
if (ase->direction == le_audio::types::kLeAudioDirectionSink) {
extra_stream << "snk,";
} else {
extra_stream << "src,";
}
/* Filter multidirectional audio context for each ase direction */
auto directional_audio_context =
context_types.get(ase->direction) &
leAudioDevice->GetAvailableContexts(ase->direction);
std::vector<uint8_t> new_metadata;
if (directional_audio_context.any()) {
new_metadata = leAudioDevice->GetMetadata(
directional_audio_context, ccid_lists.get(ase->direction));
} else {
new_metadata = leAudioDevice->GetMetadata(
AudioContexts(LeAudioContextType::UNSPECIFIED),
std::vector<uint8_t>());
}
/* Do not update if metadata did not changed. */
if (ase->metadata == new_metadata) {
continue;
}
ase->metadata = new_metadata;
struct le_audio::client_parser::ascs::ctp_update_metadata conf;
conf.ase_id = ase->id;
conf.metadata = ase->metadata;
confs.push_back(conf);
extra_stream << "meta: "
<< base::HexEncode(conf.metadata.data(),
conf.metadata.size())
<< ";;";
}
if (confs.size() != 0) {
std::vector<uint8_t> value;
le_audio::client_parser::ascs::PrepareAseCtpUpdateMetadata(confs, value);
WriteToControlPoint(leAudioDevice, value);
LOG_INFO("group_id: %d, %s", leAudioDevice->group_id_,
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
log_history_->AddLogHistory(kLogControlPointCmd, leAudioDevice->group_id_,
leAudioDevice->address_, msg_stream.str(),
extra_stream.str());
}
}
void PrepareAndSendReceiverStartReady(LeAudioDevice* leAudioDevice,
struct ase* ase) {
std::vector<uint8_t> ids;
std::vector<uint8_t> value;
std::stringstream stream;
stream << kLogAseStartReadyOp;
do {
if (ase->direction == le_audio::types::kLeAudioDirectionSource) {
stream << "ASE_ID " << +ase->id << ",";
ids.push_back(ase->id);
}
} while ((ase = leAudioDevice->GetNextActiveAse(ase)));
if (ids.size() > 0) {
le_audio::client_parser::ascs::PrepareAseCtpAudioReceiverStartReady(
ids, value);
WriteToControlPoint(leAudioDevice, value);
LOG_INFO("group_id: %d, %s", leAudioDevice->group_id_,
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_));
log_history_->AddLogHistory(kLogControlPointCmd, leAudioDevice->group_id_,
leAudioDevice->address_, stream.str());
}
}
void AseStateMachineProcessEnabling(
struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
if (!group) {
LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
return;
}
switch (ase->state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING);
if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
if (ase->cis_state < CisState::CONNECTING) {
/* We are here because of the reconnection of the single device. */
if (!CisCreateForDevice(group, leAudioDevice)) {
StopStream(group);
return;
}
}
if (!leAudioDevice->HaveAllActiveAsesCisEst()) {
/* More cis established events has to come */
return;
}
if (!leAudioDevice->IsReadyToCreateStream()) {
/* Device still remains in ready to create stream state. It means
* that more enabling status notifications has to come.
*/
return;
}
/* All CISes created. Send start ready for source ASE before we can go
* to streaming state.
*/
struct ase* ase = leAudioDevice->GetFirstActiveAse();
ASSERT_LOG(ase != nullptr,
"shouldn't be called without an active ASE, device %s",
leAudioDevice->address_.ToString().c_str());
PrepareAndSendReceiverStartReady(leAudioDevice, ase);
return;
}
if (leAudioDevice->IsReadyToCreateStream())
ProcessGroupEnable(group);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
/* Enable/Switch Content */
break;
default:
LOG(ERROR) << __func__ << ", invalid state transition, from: "
<< static_cast<int>(ase->state) << ", to: "
<< static_cast<int>(
AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING);
StopStream(group);
break;
}
}
void AseStateMachineProcessStreaming(
struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
uint8_t* data, uint16_t len, LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice) {
if (!group) {
LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
return;
}
switch (ase->state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
LOG_ERROR(
"%s, ase_id: %d, moving from QoS Configured to Streaming is "
"impossible.",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id);
group->PrintDebugState();
StopStream(group);
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING: {
std::vector<uint8_t> value;
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
if (!group->HaveAllActiveDevicesAsesTheSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING)) {
/* More ASEs notification form this device has to come for this group
*/
return;
}
if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
/* We are here because of the reconnection of the single device */
LOG_INFO("%s, Ase id: %d, ase state: %s",
ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_), ase->id,
bluetooth::common::ToString(ase->state).c_str());
cancel_watchdog_if_needed(group->group_id_);
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::STREAMING);
return;
}
/* Not all CISes establish events will came */
if (!group->IsGroupStreamReady()) {
LOG_INFO("CISes are not yet ready, wait for it.");
group->SetNotifyStreamingWhenCisesAreReadyFlag(true);
return;
}
if (group->GetTargetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
/* No more transition for group */
cancel_watchdog_if_needed(group->group_id_);
/* Last node is in streaming state */
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::STREAMING);
return;
}
LOG_ERROR(", invalid state transition, from: %s, to: %s",
ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
StopStream(group);
break;
}
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING: {
struct le_audio::client_parser::ascs::ase_transient_state_params rsp;
if (!ParseAseStatusTransientStateParams(rsp, len, data)) {
StopStream(group);
return;
}
/* Cache current set up metadata values for for further possible
* reconfiguration
*/
if (!rsp.metadata.empty()) {
ase->metadata = rsp.metadata;
}
break;
}
default:
LOG(ERROR) << __func__ << ", invalid state transition, from: "
<< static_cast<int>(ase->state) << ", to: "
<< static_cast<int>(
AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
StopStream(group);
break;
}
}
void ScheduleAutonomousOperationTimer(AseState target_state,
LeAudioDevice* leAudioDevice,
struct ase* ase) {
ase->autonomous_target_state_ = target_state;
ase->autonomous_operation_timer_ =
alarm_new("LeAudioAutonomousOperationTimeout");
alarm_set_on_mloop(
ase->autonomous_operation_timer_, kAutonomousTransitionTimeoutMs,
[](void* data) {
LeAudioDevice* leAudioDevice = static_cast<LeAudioDevice*>(data);
instance->state_machine_callbacks_
->OnDeviceAutonomousStateTransitionTimeout(leAudioDevice);
},
leAudioDevice);
}
void AseStateMachineProcessDisabling(
struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
if (!group) {
LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
return;
}
if (ase->direction == le_audio::types::kLeAudioDirectionSink) {
/* Sink ASE state machine does not have Disabling state */
LOG_ERROR(", invalid state transition, from: %s , to: %s ",
ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
StopStream(group);
return;
}
switch (ase->state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING:
/* TODO: Disable */
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING);
/* Remote may autonomously bring ASEs to QoS configured state */
if (group->GetTargetState() !=
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) {
ProcessAutonomousDisable(leAudioDevice, ase);
}
/* Process the Disable Transition of the rest of group members if no
* more ASE notifications has to come from this device. */
if (leAudioDevice->IsReadyToSuspendStream()) ProcessGroupDisable(group);
break;
default:
LOG(ERROR) << __func__ << ", invalid state transition, from: "
<< static_cast<int>(ase->state) << ", to: "
<< static_cast<int>(
AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING);
StopStream(group);
break;
}
}
void DisconnectCisIfNeeded(LeAudioDeviceGroup* group,
LeAudioDevice* leAudioDevice, struct ase* ase) {
LOG_DEBUG(
"Group id: %d, %s, ase id: %d, cis_handle: 0x%04x, direction: %s, "
"data_path_state: %s, cis_state: %s",
group->group_id_, ADDRESS_TO_LOGGABLE_CSTR(leAudioDevice->address_),
ase->id, ase->cis_conn_hdl,
ase->direction == le_audio::types::kLeAudioDirectionSink ? "sink"
: "source",
bluetooth::common::ToString(ase->data_path_state).c_str(),
bluetooth::common::ToString(ase->cis_state).c_str());
auto bidirection_ase = leAudioDevice->GetAseToMatchBidirectionCis(ase);
if (bidirection_ase != nullptr &&
bidirection_ase->cis_state == CisState::CONNECTED &&
(bidirection_ase->state == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING ||
bidirection_ase->state == AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING)) {
LOG_INFO("Still waiting for the bidirectional ase %d to be released (%s)",
bidirection_ase->id,
bluetooth::common::ToString(bidirection_ase->state).c_str());
return;
}
group->RemoveCisFromStreamIfNeeded(leAudioDevice, ase->cis_conn_hdl);
IsoManager::GetInstance()->DisconnectCis(ase->cis_conn_hdl,
HCI_ERR_PEER_USER);
log_history_->AddLogHistory(
kLogStateMachineTag, group->group_id_, leAudioDevice->address_,
kLogCisDisconnectOp + "cis_h:" + loghex(ase->cis_conn_hdl));
}
void AseStateMachineProcessReleasing(
struct le_audio::client_parser::ascs::ase_rsp_hdr& arh, struct ase* ase,
LeAudioDeviceGroup* group, LeAudioDevice* leAudioDevice) {
if (!group) {
LOG(ERROR) << __func__ << ", leAudioDevice doesn't belong to any group";
return;
}
switch (ase->state) {
case AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING:
case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING);
if (group->HaveAllActiveDevicesAsesTheSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING)) {
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING);
}
if (group->cig.GetState() == CigState::CREATED &&
group->HaveAllCisesDisconnected()) {
RemoveCigForGroup(group);
}
break;
case AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING: {
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING);
bool remove_cig = true;
/* Happens when bi-directional completive ASE releasing state came */
if (ase->cis_state == CisState::DISCONNECTING) break;
if ((ase->cis_state == CisState::CONNECTED ||
ase->cis_state == CisState::CONNECTING) &&
ase->data_path_state == DataPathState::IDLE) {
DisconnectCisIfNeeded(group, leAudioDevice, ase);
/* CISes are still there. CIG will be removed when CIS is down. */
remove_cig = false;
}
if (!group->HaveAllActiveDevicesAsesTheSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING)) {
return;
}
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING);
if (remove_cig) {
/* In the ENABLING state most probably there was no CISes created.
* Make sure group is destroyed here */
RemoveCigForGroup(group);
}
break;
}
case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING: {
SetAseState(leAudioDevice, ase,
AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING);
/* Happens when bi-directional completive ASE releasing state came */
if (ase->cis_state == CisState::DISCONNECTING) break;
if (ase->data_path_state == DataPathState::CONFIGURED) {
RemoveDataPathByCisHandle(leAudioDevice, ase->cis_conn_hdl);
} else if ((ase->cis_state == CisState::CONNECTED ||
ase->cis_state == CisState::CONNECTING) &&
ase->data_path_state == DataPathState::IDLE) {
DisconnectCisIfNeeded(group, leAudioDevice, ase);
} else {
DLOG(INFO) << __func__ << ", Nothing to do ase data path state: "
<< static_cast<int>(ase->data_path_state);
}
break;
}
default:
LOG(ERROR) << __func__ << ", invalid state transition, from: "
<< static_cast<int>(ase->state) << ", to: "
<< static_cast<int>(
AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING);
break;
}
}
void ProcessGroupEnable(LeAudioDeviceGroup* group) {
if (group->GetState() != AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING) {
/* Check if the group is ready to create stream. If not, keep waiting. */
if (!group->IsGroupReadyToCreateStream()) {
LOG_DEBUG(
"Waiting for more ASEs to be in enabling or directly in streaming "
"state");
return;
}
/* Group can move to Enabling state now. */
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_ENABLING);
}
/* If Target State is not streaming, then something is wrong. */
if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
LOG_ERROR(", invalid state transition, from: %s , to: %s ",
ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
StopStream(group);
return;
}
/* Try to create CISes for the group */
if (!CisCreate(group)) {
StopStream(group);
}
}
void ProcessGroupDisable(LeAudioDeviceGroup* group) {
/* Disable ASEs for next device in group. */
if (group->GetState() != AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING) {
if (!group->IsGroupReadyToSuspendStream()) {
LOG_INFO("Waiting for all devices to be in disable state");
return;
}
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_DISABLING);
}
/* At this point all of the active ASEs within group are disabled. As there
* is no Disabling state for Sink ASE, it might happen that all of the
* active ASEs are Sink ASE and will transit to QoS state. So check
* the group state, because we might be ready to release data path. */
if (group->HaveAllActiveDevicesAsesTheSameState(
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED)) {
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
}
/* Transition to QoS configured is done by CIS disconnection */
if (group->GetTargetState() ==
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) {
ReleaseDataPath(group);
} else {
LOG_ERROR(", invalid state transition, from: %s , to: %s ",
ToString(group->GetState()).c_str(),
ToString(group->GetTargetState()).c_str());
StopStream(group);
}
}
void ProcessAutonomousDisable(LeAudioDevice* leAudioDevice, struct ase* ase) {
auto bidirection_ase = leAudioDevice->GetAseToMatchBidirectionCis(ase);
/* ASE is not a part of bi-directional CIS */
if (!bidirection_ase) return;
/* ASE is already disabled */
if (bidirection_ase->state ==
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) {
/* Bi-direction ASEs are now disabled */
if ((ase->autonomous_target_state_ ==
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) &&
alarm_is_scheduled(ase->autonomous_operation_timer_)) {
alarm_free(ase->autonomous_operation_timer_);
ase->autonomous_operation_timer_ = NULL;
ase->autonomous_target_state_ = AseState::BTA_LE_AUDIO_ASE_STATE_IDLE;
}
return;
}
/* Schedule alarm if first ASE is autonomously disabling */
if (!alarm_is_scheduled(bidirection_ase->autonomous_operation_timer_)) {
ScheduleAutonomousOperationTimer(
AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED, leAudioDevice,
bidirection_ase);
}
}
};
} // namespace
namespace le_audio {
void LeAudioGroupStateMachine::Initialize(Callbacks* state_machine_callbacks_) {
if (instance) {
LOG(ERROR) << "Already initialized";
return;
}
instance = new LeAudioGroupStateMachineImpl(state_machine_callbacks_);
}
void LeAudioGroupStateMachine::Cleanup() {
if (!instance) return;
LeAudioGroupStateMachineImpl* ptr = instance;
instance = nullptr;
delete ptr;
}
LeAudioGroupStateMachine* LeAudioGroupStateMachine::Get() {
CHECK(instance);
return instance;
}
} // namespace le_audio