/* * Copyright 2019 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. */ /* * This module contains API of the audio stream control protocol. */ #include "client_parser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "le_audio_types.h" #include "le_audio_utils.h" #include "stack/include/bt_types.h" using bluetooth::le_audio::types::acs_ac_record; namespace bluetooth::le_audio { namespace client_parser { namespace ascs { static std::map ase_state_map_string = { {kAseStateIdle, "Idle"}, {kAseStateCodecConfigured, "Codec Configured"}, {kAseStateQosConfigured, "QoS Configured"}, {kAseStateEnabling, "Enabling"}, {kAseStateStreaming, "Streaming"}, {kAseStateDisabling, "Disabling"}, {kAseStateReleasing, "Releasing"}, }; static std::map ctp_opcode_map_string = { {kCtpOpcodeCodecConfiguration, "Config Codec"}, {kCtpOpcodeQosConfiguration, "Config QoS"}, {kCtpOpcodeEnable, "Enable"}, {kCtpOpcodeReceiverStartReady, "Receiver Start Ready"}, {kCtpOpcodeDisable, "Disable"}, {kCtpOpcodeReceiverStopReady, "Receiver Stop Ready"}, {kCtpOpcodeUpdateMetadata, "Update Metadata"}, {kCtpOpcodeRelease, "Release"}, }; static std::map ctp_configuration_reason_map_string = { {kCtpResponseNoReason, ""}, {kCtpResponseCodecId, "Codec ID"}, {kCtpResponseCodecSpecificConfiguration, "Codec specific configuration"}, {kCtpResponseSduInterval, "SDU interval"}, {kCtpResponseFraming, "Framing"}, {kCtpResponsePhy, "PHY"}, {kCtpResponseMaximumSduSize, "Maximum SDU size"}, {kCtpResponseRetransmissionNumber, "Retransmission number"}, {kCtpResponseMaxTransportLatency, "Max Transport latency"}, {kCtpResponsePresentationDelay, "Presentation delay"}, {kCtpResponseInvalidAseCisMapping, "Invalid ASE CIS mapping"}, }; static std::map ctp_response_code_map_string = { {kCtpResponseCodeSuccess, "Success"}, {kCtpResponseCodeUnsupportedOpcode, "Unsupported Opcode"}, {kCtpResponseCodeInvalidLength, "Invalid Length"}, {kCtpResponseCodeInvalidAseId, "Invalid ASE ID"}, {kCtpResponseCodeInvalidAseStateMachineTransition, "Invalid ASE State Machine Transition"}, {kCtpResponseCodeInvalidAseDirection, "Invalid ASE Direction"}, {kCtpResponseCodeUnsupportedAudioCapabilities, "Unsupported Audio Capabilities"}, {kCtpResponseCodeUnsupportedConfigurationParameterValue, "Unsupported Configuration Parameter Value"}, {kCtpResponseCodeRejectedConfigurationParameterValue, "Rejected Configuration Parameter Value"}, {kCtpResponseCodeInvalidConfigurationParameterValue, "Invalid Configuration Parameter Value"}, {kCtpResponseCodeUnsupportedMetadata, "Unsupported Metadata"}, {kCtpResponseCodeRejectedMetadata, "Rejected Metadata"}, {kCtpResponseCodeInvalidMetadata, "Invalid Metadata"}, {kCtpResponseCodeInsufficientResources, "Insufficient Resources"}, {kCtpResponseCodeUnspecifiedError, "Unspecified Error"}, }; static std::map ctp_metadata_reason_map_string = { {kCtpMetadataResponsePreferredAudioContexts, "Preferred Audio Contexts"}, {kCtpMetadataResponseStreamingAudioContexts, "Streaming Audio Contexts"}, {kCtpMetadataResponseProgramInfo, "Program Info"}, {kCtpMetadataResponseLanguage, "Language"}, {kCtpMetadataResponseCcidList, "CCID List"}, {kCtpMetadataResponseParentalRating, "Parental Rating"}, {kCtpMetadataResponseProgramInfoUri, "Program Info URI"}, {kCtpMetadataResponseExtendedMetadata, "Extended Metadata"}, {kCtpMetadataResponseVendorSpecific, "Vendor Specific"}, }; static std::map*> ctp_response_code_map = { {kCtpResponseCodeUnsupportedConfigurationParameterValue, &ctp_configuration_reason_map_string}, {kCtpResponseCodeRejectedConfigurationParameterValue, &ctp_configuration_reason_map_string}, {kCtpResponseCodeInvalidConfigurationParameterValue, &ctp_configuration_reason_map_string}, {kCtpResponseCodeUnsupportedMetadata, &ctp_metadata_reason_map_string}, {kCtpResponseCodeRejectedMetadata, &ctp_metadata_reason_map_string}, {kCtpResponseCodeInvalidMetadata, &ctp_metadata_reason_map_string}, }; bool ParseAseStatusHeader(ase_rsp_hdr& arh, uint16_t len, const uint8_t* value) { if (len < kAseRspHdrMinLen) { log::error("wrong len of ASE char (header): {}", static_cast(len)); return false; } STREAM_TO_UINT8(arh.id, value); STREAM_TO_UINT8(arh.state, value); log::info("ASE status: \tASE id: 0x{:x}\tASE state: {} (0x{:x})", arh.id, ase_state_map_string[arh.state], arh.state); return true; } bool ParseAseStatusCodecConfiguredStateParams(struct ase_codec_configured_state_params& rsp, uint16_t len, const uint8_t* value) { uint8_t codec_spec_conf_len; if (len < kAseStatusCodecConfMinLen) { log::error("Wrong len of codec conf status (Codec conf header)"); return false; } STREAM_TO_UINT8(rsp.framing, value); STREAM_TO_UINT8(rsp.preferred_phy, value); STREAM_TO_UINT8(rsp.preferred_retrans_nb, value); STREAM_TO_UINT16(rsp.max_transport_latency, value); STREAM_TO_UINT24(rsp.pres_delay_min, value); STREAM_TO_UINT24(rsp.pres_delay_max, value); STREAM_TO_UINT24(rsp.preferred_pres_delay_min, value); STREAM_TO_UINT24(rsp.preferred_pres_delay_max, value); STREAM_TO_UINT8(rsp.codec_id.coding_format, value); STREAM_TO_UINT16(rsp.codec_id.vendor_company_id, value); STREAM_TO_UINT16(rsp.codec_id.vendor_codec_id, value); STREAM_TO_UINT8(codec_spec_conf_len, value); len -= kAseStatusCodecConfMinLen; if (len != codec_spec_conf_len) { log::error("Wrong len of codec conf status (Codec spec conf)"); return false; } if (codec_spec_conf_len) { rsp.codec_spec_conf = std::vector(value, value + codec_spec_conf_len); } log::info( "Codec configuration\n\tFraming: 0x{:x}\n\tPreferred PHY: " "0x{:x}\n\tPreferred retransmission number: 0x{:x}\n\tMax transport " "latency: 0x{:x}\n\tPresence delay min: 0x{:x}\n\tPresence delay max: " "0x{:x}\n\tPreferredPresentationDelayMin: " "0x{:x}\n\tPreferredPresentationDelayMax: 0x{:x}\n\tCoding format: " "0x{:x}\n\tVendor codec company ID: 0x{:x}\n\tVendor codec ID: " "0x{:x}\n\tCodec specific conf len: {}\n\tCodec specific conf: {}", rsp.framing, rsp.preferred_phy, rsp.preferred_retrans_nb, rsp.max_transport_latency, rsp.pres_delay_min, rsp.pres_delay_max, rsp.preferred_pres_delay_min, rsp.preferred_pres_delay_max, rsp.codec_id.coding_format, rsp.codec_id.vendor_company_id, rsp.codec_id.vendor_codec_id, (int)codec_spec_conf_len, base::HexEncode(rsp.codec_spec_conf.data(), rsp.codec_spec_conf.size())); return true; } bool ParseAseStatusQosConfiguredStateParams(struct ase_qos_configured_state_params& rsp, uint16_t len, const uint8_t* value) { if (len != kAseStatusCodecQosConfMinLen) { log::error("Wrong len of ASE characteristic (QOS conf header)"); return false; } STREAM_TO_UINT8(rsp.cig_id, value); STREAM_TO_UINT8(rsp.cis_id, value); STREAM_TO_UINT24(rsp.sdu_interval, value); STREAM_TO_UINT8(rsp.framing, value); STREAM_TO_UINT8(rsp.phy, value); STREAM_TO_UINT16(rsp.max_sdu, value); STREAM_TO_UINT8(rsp.retrans_nb, value); STREAM_TO_UINT16(rsp.max_transport_latency, value); STREAM_TO_UINT24(rsp.pres_delay, value); log::info( "Codec QoS Configured\n\tCIG: 0x{:x}\n\tCIS: 0x{:x}\n\tSDU interval: " "0x{:x}\n\tFraming: 0x{:x}\n\tPHY: 0x{:x}\n\tMax SDU: " "0x{:x}\n\tRetransmission number: 0x{:x}\n\tMax transport latency: " "0x{:x}\n\tPresentation delay: 0x{:x}", rsp.cig_id, rsp.cis_id, rsp.sdu_interval, rsp.framing, rsp.phy, rsp.max_sdu, rsp.retrans_nb, rsp.max_transport_latency, rsp.pres_delay); return true; } bool ParseAseStatusTransientStateParams(struct ase_transient_state_params& rsp, uint16_t len, const uint8_t* value) { uint8_t metadata_len; if (len < kAseStatusTransMinLen) { log::error("Wrong len of ASE characteristic (metadata)"); return false; } STREAM_TO_UINT8(rsp.cig_id, value); STREAM_TO_UINT8(rsp.cis_id, value); STREAM_TO_UINT8(metadata_len, value); len -= kAseStatusTransMinLen; if (len != metadata_len) { log::error("Wrong len of ASE characteristic (metadata)"); return false; } if (metadata_len > 0) { rsp.metadata = std::vector(value, value + metadata_len); } log::info( "Status enabling/streaming/disabling\n\tCIG: 0x{:x}\n\tCIS: " "0x{:x}\n\tMetadata: {}", rsp.cig_id, rsp.cis_id, base::HexEncode(rsp.metadata.data(), rsp.metadata.size())); return true; } bool ParseAseCtpNotification(struct ctp_ntf& ntf, uint16_t len, const uint8_t* value) { uint8_t num_entries; if (len < kCtpNtfMinLen) { log::error("Wrong len of ASE control point notification: {}", (int)len); return false; } STREAM_TO_UINT8(ntf.op, value); STREAM_TO_UINT8(num_entries, value); if (len != kCtpNtfMinLen + (num_entries * kCtpAseEntryMinLen)) { log::error("Wrong len of ASE control point notification (ASE IDs)"); return false; } for (int i = 0; i < num_entries; i++) { struct ctp_ase_entry entry; STREAM_TO_UINT8(entry.ase_id, value); STREAM_TO_UINT8(entry.response_code, value); STREAM_TO_UINT8(entry.reason, value); ntf.entries.push_back(std::move(entry)); } log::info("Control point notification\n\tOpcode: {} (0x{:x})\n\tNum ASE IDs: {}", ctp_opcode_map_string[ntf.op], ntf.op, (int)num_entries); for (size_t i = 0; i < num_entries; i++) { log::info( "\n\tASE ID[0x{:x}] response: {} (0x{:x}) reason: {} (0x{:x})", ntf.entries[i].ase_id, ctp_response_code_map_string[ntf.entries[i].response_code], ntf.entries[i].response_code, ((ctp_response_code_map.count(ntf.entries[i].response_code) != 0) ? (*ctp_response_code_map[ntf.entries[i].response_code])[ntf.entries[i].reason] : ""), ntf.entries[i].reason); } return true; } bool PrepareAseCtpCodecConfig(const std::vector& confs, std::vector& value) { if (confs.size() == 0) { log::error("Configuration set is empty!"); return false; } std::stringstream conf_ents_str; size_t msg_len = std::accumulate( confs.begin(), confs.end(), confs.size() * kCtpCodecConfMinLen + kAseNumSize + kCtpOpSize, [&conf_ents_str](size_t cur_len, auto const& conf) { if (utils::IsCodecUsingLtvFormat(conf.codec_id)) { types::LeAudioLtvMap ltv; if (ltv.Parse(conf.codec_config.data(), conf.codec_config.size())) { for (const auto& [type, value] : ltv.Values()) { conf_ents_str << "\ttype: " << std::to_string(type) << "\tlen: " << std::to_string(value.size()) << "\tdata: " << base::HexEncode(value.data(), value.size()) << "\n"; } return cur_len + conf.codec_config.size(); } log::error("Error parsing codec configuration LTV data."); } conf_ents_str << "\t" << base::HexEncode(conf.codec_config.data(), conf.codec_config.size()); return cur_len + conf.codec_config.size(); }); value.resize(msg_len); uint8_t* msg = value.data(); UINT8_TO_STREAM(msg, kCtpOpcodeCodecConfiguration); UINT8_TO_STREAM(msg, confs.size()); for (const struct ctp_codec_conf& conf : confs) { UINT8_TO_STREAM(msg, conf.ase_id); UINT8_TO_STREAM(msg, conf.target_latency); UINT8_TO_STREAM(msg, conf.target_phy); UINT8_TO_STREAM(msg, conf.codec_id.coding_format); UINT16_TO_STREAM(msg, conf.codec_id.vendor_company_id); UINT16_TO_STREAM(msg, conf.codec_id.vendor_codec_id); UINT8_TO_STREAM(msg, conf.codec_config.size()); ARRAY_TO_STREAM(msg, conf.codec_config.data(), static_cast(conf.codec_config.size())); log::info( "Codec configuration\n\tAse id: 0x{:x}\n\tTarget latency: " "0x{:x}\n\tTarget PHY: 0x{:x}\n\tCoding format: 0x{:x}\n\tVendor codec " "company ID: 0x{:x}\n\tVendor codec ID: 0x{:x}\n\tCodec config len: " "{}\n\tCodec spec conf: \n{}", conf.ase_id, conf.target_latency, conf.target_phy, conf.codec_id.coding_format, conf.codec_id.vendor_company_id, conf.codec_id.vendor_codec_id, static_cast(conf.codec_config.size()), conf_ents_str.str()); } return true; } bool PrepareAseCtpConfigQos(const std::vector& confs, std::vector& value) { if (confs.size() == 0) { log::error("Configuration set is empty!"); return false; } value.resize(confs.size() * kCtpQosConfMinLen + kAseNumSize + kCtpOpSize); uint8_t* msg = value.data(); UINT8_TO_STREAM(msg, kCtpOpcodeQosConfiguration); UINT8_TO_STREAM(msg, confs.size()); for (const struct ctp_qos_conf& conf : confs) { UINT8_TO_STREAM(msg, conf.ase_id); UINT8_TO_STREAM(msg, conf.cig); UINT8_TO_STREAM(msg, conf.cis); UINT24_TO_STREAM(msg, conf.sdu_interval); UINT8_TO_STREAM(msg, conf.framing); UINT8_TO_STREAM(msg, conf.phy); UINT16_TO_STREAM(msg, conf.max_sdu); UINT8_TO_STREAM(msg, conf.retrans_nb); UINT16_TO_STREAM(msg, conf.max_transport_latency); UINT24_TO_STREAM(msg, conf.pres_delay); log::info( "QoS configuration\n\tAse id: 0x{:x}\n\tcig: 0x{:x}\n\tCis: " "0x{:x}\n\tSDU interval: 0x{:x}\n\tFraming: 0x{:x}\n\tPhy: " "0x{:x}\n\tMax sdu size: 0x{:x}\n\tRetrans nb: 0x{:x}\n\tMax Transport " "latency: 0x{:x}\n\tPres delay: 0x{:x}", conf.ase_id, conf.cig, conf.cis, conf.sdu_interval, conf.framing, conf.phy, conf.max_sdu, conf.retrans_nb, conf.max_transport_latency, conf.pres_delay); } return true; } bool PrepareAseCtpEnable(const std::vector& confs, std::vector& value) { if (confs.size() == 0) { log::error("Configuration set is empty!"); return false; } if (confs.size() > UINT8_MAX) { log::error("To many ASEs to update metadata"); return false; } uint16_t msg_len = confs.size() * kCtpEnableMinLen + kAseNumSize + kCtpOpSize; for (auto& conf : confs) { if (msg_len > GATT_MAX_ATTR_LEN) { log::error("Message length above GATT maximum"); return false; } if (conf.metadata.size() > UINT8_MAX) { log::error("ase[{}] metadata length is invalid", conf.ase_id); return false; } msg_len += conf.metadata.size(); } value.resize(msg_len); uint8_t* msg = value.data(); UINT8_TO_STREAM(msg, kCtpOpcodeEnable); UINT8_TO_STREAM(msg, confs.size()); for (const struct ctp_enable& conf : confs) { UINT8_TO_STREAM(msg, conf.ase_id); UINT8_TO_STREAM(msg, conf.metadata.size()); ARRAY_TO_STREAM(msg, conf.metadata.data(), static_cast(conf.metadata.size())); log::info("Enable\n\tAse id: 0x{:x}\n\tMetadata: {}", conf.ase_id, base::HexEncode(conf.metadata.data(), conf.metadata.size())); } return true; } bool PrepareAseCtpAudioReceiverStartReady(const std::vector& ase_ids, std::vector& value) { if (ase_ids.size() == 0) { log::error("ASEs ID set is empty!"); return false; } value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize); uint8_t* msg = value.data(); UINT8_TO_STREAM(msg, kCtpOpcodeReceiverStartReady); UINT8_TO_STREAM(msg, ase_ids.size()); for (const uint8_t& id : ase_ids) { UINT8_TO_STREAM(msg, id); log::info("ReceiverStartReady\n\tAse id: 0x{:x}", id); } return true; } bool PrepareAseCtpDisable(const std::vector& ase_ids, std::vector& value) { if (ase_ids.size() == 0) { log::error("ASEs ID set is empty!"); return false; } value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize); uint8_t* msg = value.data(); UINT8_TO_STREAM(msg, kCtpOpcodeDisable); UINT8_TO_STREAM(msg, ase_ids.size()); for (const uint8_t& id : ase_ids) { UINT8_TO_STREAM(msg, id); log::info("Disable\n\tAse id: 0x{:x}", id); } return true; } bool PrepareAseCtpAudioReceiverStopReady(const std::vector& ase_ids, std::vector& value) { if (ase_ids.size() == 0) { log::error("ASEs ID set is empty!"); return false; } value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize); uint8_t* msg = value.data(); UINT8_TO_STREAM(msg, kCtpOpcodeReceiverStopReady); UINT8_TO_STREAM(msg, ase_ids.size()); for (const uint8_t& ase_id : ase_ids) { UINT8_TO_STREAM(msg, ase_id); log::info("ReceiverStopReady\n\tAse id: 0x{:x}", ase_id); } return true; } bool PrepareAseCtpUpdateMetadata(const std::vector& confs, std::vector& value) { if (confs.size() == 0) { log::error("Configuration set is empty!"); return false; } if (confs.size() > UINT8_MAX) { log::error("To many ASEs to update metadata"); return false; } uint16_t msg_len = confs.size() * kCtpUpdateMetadataMinLen + kAseNumSize + kCtpOpSize; for (auto& conf : confs) { if (msg_len > GATT_MAX_ATTR_LEN) { log::error("Message length above GATT maximum"); return false; } if (conf.metadata.size() > UINT8_MAX) { log::error("ase[{}] metadata length is invalid", conf.ase_id); return false; } msg_len += conf.metadata.size(); } value.resize(msg_len); uint8_t* msg = value.data(); UINT8_TO_STREAM(msg, kCtpOpcodeUpdateMetadata); UINT8_TO_STREAM(msg, confs.size()); for (const struct ctp_update_metadata& conf : confs) { UINT8_TO_STREAM(msg, conf.ase_id); UINT8_TO_STREAM(msg, conf.metadata.size()); ARRAY_TO_STREAM(msg, conf.metadata.data(), static_cast(conf.metadata.size())); log::info("Update Metadata\n\tAse id: 0x{:x}\n\tMetadata: {}", conf.ase_id, base::HexEncode(conf.metadata.data(), conf.metadata.size())); } return true; } bool PrepareAseCtpRelease(const std::vector& ase_ids, std::vector& value) { if (ase_ids.size() == 0) { return true; } value.resize(ase_ids.size() * kAseIdSize + kAseNumSize + kCtpOpSize); uint8_t* msg = value.data(); UINT8_TO_STREAM(msg, kCtpOpcodeRelease); UINT8_TO_STREAM(msg, ase_ids.size()); for (const uint8_t& ase_id : ase_ids) { UINT8_TO_STREAM(msg, ase_id); log::info("Release\n\tAse id: 0x{:x}", ase_id); } return true; } } // namespace ascs namespace pacs { int ParseSinglePac(std::vector& pac_recs, uint16_t len, const uint8_t* value) { struct acs_ac_record rec; uint8_t codec_spec_cap_len, metadata_len; if (len < kAcsPacRecordMinLen) { log::error("Wrong len of PAC record ({}!={})", len, kAcsPacRecordMinLen); pac_recs.clear(); return -1; } STREAM_TO_UINT8(rec.codec_id.coding_format, value); STREAM_TO_UINT16(rec.codec_id.vendor_company_id, value); STREAM_TO_UINT16(rec.codec_id.vendor_codec_id, value); STREAM_TO_UINT8(codec_spec_cap_len, value); len -= kAcsPacRecordMinLen - kAcsPacMetadataLenLen; if (len < codec_spec_cap_len + kAcsPacMetadataLenLen) { log::error("Wrong len of PAC record (codec specific capabilities) ({}!={})", len, codec_spec_cap_len + kAcsPacMetadataLenLen); pac_recs.clear(); return -1; } rec.codec_spec_caps_raw.assign(value, value + codec_spec_cap_len); if (utils::IsCodecUsingLtvFormat(rec.codec_id)) { if (!rec.codec_spec_caps.Parse(value, codec_spec_cap_len)) { log::error("Unable to parse codec specific parameters LTV: {}", base::HexEncode(value, codec_spec_cap_len)); return -1; } } value += codec_spec_cap_len; len -= codec_spec_cap_len; STREAM_TO_UINT8(metadata_len, value); len -= kAcsPacMetadataLenLen; if (len < metadata_len) { log::error("Wrong len of PAC record (metadata) ({}!={})", len, metadata_len); pac_recs.clear(); return -1; } if (!rec.metadata.Parse(value, metadata_len)) { log::error("PAC record contains corrupted LTV formatted metadata: {}", base::HexEncode(value, metadata_len)); return -1; } value += metadata_len; len -= metadata_len; pac_recs.push_back(std::move(rec)); return len; } bool ParsePacs(std::vector& pac_recs, uint16_t len, const uint8_t* value) { if (len < kAcsPacDiscoverRspMinLen) { log::error("Wrong len of PAC characteristic ({}!={})", len, kAcsPacDiscoverRspMinLen); return false; } uint8_t pac_rec_nb; STREAM_TO_UINT8(pac_rec_nb, value); len -= kAcsPacDiscoverRspMinLen; pac_recs.reserve(pac_rec_nb); for (int i = 0; i < pac_rec_nb; i++) { int remaining_len = ParseSinglePac(pac_recs, len, value); if (remaining_len < 0) { log::error("Error parsing PAC records."); return false; } value += (len - remaining_len); len = remaining_len; } return true; } bool ParseAudioLocations(types::AudioLocations& audio_locations, uint16_t len, const uint8_t* value) { if (len != kAudioLocationsRspMinLen) { log::error("Wrong len of Audio Location characteristic"); return false; } STREAM_TO_UINT32(audio_locations, value); log::info("Audio locations: {}", audio_locations.to_string()); return true; } bool ParseSupportedAudioContexts(types::BidirectionalPair& contexts, uint16_t len, const uint8_t* value) { if (len != kAseAudioSuppContRspMinLen) { log::error("Wrong len of Audio Supported Context characteristic"); return false; } STREAM_TO_UINT16(contexts.sink.value_ref(), value); STREAM_TO_UINT16(contexts.source.value_ref(), value); log::info( "Supported Audio Contexts: \n\tSupported Sink Contexts: {}\n\tSupported " "Source Contexts: {}", contexts.sink.to_string(), contexts.source.to_string()); return true; } bool ParseAvailableAudioContexts(types::BidirectionalPair& contexts, uint16_t len, const uint8_t* value) { if (len != kAseAudioAvailRspMinLen) { log::error("Wrong len of Audio Availability characteristic"); return false; } STREAM_TO_UINT16(contexts.sink.value_ref(), value); STREAM_TO_UINT16(contexts.source.value_ref(), value); log::info( "Available Audio Contexts: \n\tAvailable Sink Contexts: {}\n\tAvailable " "Source Contexts: {}", contexts.sink.to_string(), contexts.source.to_string()); return true; } } // namespace pacs namespace tmap { bool ParseTmapRole(std::bitset<16>& role, uint16_t len, const uint8_t* value) { if (len != kTmapRoleLen) { log::error(", Wrong len of Telephony Media Audio Profile Role, characteristic"); return false; } STREAM_TO_UINT16(role, value); log::info(", Telephony Media Audio Profile Role:\n\tRole: {}", role.to_string()); return true; } } // namespace tmap } // namespace client_parser } // namespace bluetooth::le_audio