/* * Copyright (C) 2023 BlueKitchen GmbH * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holders nor the names of * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * 4. Any redistribution, use, or modification is done solely for * personal benefit and not for any commercial purpose or for * monetary gain. * * THIS SOFTWARE IS PROVIDED BY BLUEKITCHEN GMBH AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLUEKITCHEN * GMBH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * Please inquire about commercial licensing options at * contact@bluekitchen-gmbh.com * */ #define BTSTACK_FILE__ "avrcp_cover_art_client.c" #include #include "btstack_debug.h" #include "btstack_event.h" #include "classic/avrcp.h" #include "classic/avrcp_cover_art_client.h" #include "classic/obex.h" #include "hci_event.h" #ifdef ENABLE_AVRCP_COVER_ART static const hci_event_t avrcp_cover_art_client_connected = { .event_code = HCI_EVENT_AVRCP_META, .subevent_code = AVRCP_SUBEVENT_COVER_ART_CONNECTION_ESTABLISHED, .format = "1B22" }; static const hci_event_t avrcp_cover_art_client_disconnected = { .event_code = HCI_EVENT_AVRCP_META, .subevent_code = AVRCP_SUBEVENT_COVER_ART_CONNECTION_RELEASED, .format = "2" }; static const hci_event_t avrcp_cover_art_client_operation_complete = { .event_code = HCI_EVENT_AVRCP_META, .subevent_code = AVRCP_SUBEVENT_COVER_ART_OPERATION_COMPLETE, .format = "21" }; // 7163DD54-4A7E-11E2-B47C-0050C2490048 static const uint8_t avrcp_cover_art_uuid[] = { 0x71, 0x63, 0xDD, 0x54, 0x4A, 0x7E, 0x11, 0xE2, 0xB4, 0x7C, 0x00, 0x50, 0xC2, 0x49, 0x00, 0x48 }; // OBEX types const char * avrcp_cover_art_image_properties_type = "x-bt/img-properties"; const char * avrcp_cover_art_image_type = "x-bt/img-img"; const char * avrcp_cover_art_linked_thumbnail_type = "x-bt/img-thm"; static btstack_linked_list_t avrcp_cover_art_client_connections; static uint16_t avrcp_cover_art_client_next_cid(void) { static uint16_t cid = 0; cid++; if (cid == 0){ cid = 1; } return cid; } static avrcp_cover_art_client_t * avrcp_cover_art_client_for_goep_cid(uint16_t goep_cid){ btstack_linked_list_iterator_t it; btstack_linked_list_iterator_init(&it, (btstack_linked_list_t *) &avrcp_cover_art_client_connections); while (btstack_linked_list_iterator_has_next(&it)){ avrcp_cover_art_client_t * connection = (avrcp_cover_art_client_t *)btstack_linked_list_iterator_next(&it); if (connection->goep_cid == goep_cid) { return connection; }; } return NULL; } static avrcp_cover_art_client_t * avrcp_cover_art_client_for_avrcp_cid(uint16_t avrcp_cid){ btstack_linked_list_iterator_t it; btstack_linked_list_iterator_init(&it, (btstack_linked_list_t *) &avrcp_cover_art_client_connections); while (btstack_linked_list_iterator_has_next(&it)){ avrcp_cover_art_client_t * connection = (avrcp_cover_art_client_t *)btstack_linked_list_iterator_next(&it); if (connection->avrcp_cid == avrcp_cid) { return connection; }; } return NULL; } static avrcp_cover_art_client_t * avrcp_cover_art_client_for_cover_art_cid(uint16_t cover_art_cid){ btstack_linked_list_iterator_t it; btstack_linked_list_iterator_init(&it, (btstack_linked_list_t *) &avrcp_cover_art_client_connections); while (btstack_linked_list_iterator_has_next(&it)){ avrcp_cover_art_client_t * connection = (avrcp_cover_art_client_t *)btstack_linked_list_iterator_next(&it); if (connection->cover_art_cid == cover_art_cid) { return connection; }; } return NULL; } static void avrcp_cover_art_client_emit_connection_established(btstack_packet_handler_t packet_handler, uint8_t status, bd_addr_t addr, uint16_t avrcp_cid, uint16_t cover_art_cid) { uint8_t buffer[20]; uint16_t size = hci_event_create_from_template_and_arguments(buffer, sizeof(buffer), &avrcp_cover_art_client_connected, status, addr, avrcp_cid, cover_art_cid); (*packet_handler)(HCI_EVENT_PACKET, 0, buffer, size); } static void cover_art_client_emit_operation_complete_event(avrcp_cover_art_client_t * cover_art_client, uint8_t status) { uint8_t buffer[20]; uint16_t size = hci_event_create_from_template_and_arguments(buffer, sizeof(buffer), &avrcp_cover_art_client_operation_complete, cover_art_client->cover_art_cid, status); (*cover_art_client->packet_handler)(HCI_EVENT_PACKET, 0, buffer, size); } static void avrcp_cover_art_client_emit_connection_released(btstack_packet_handler_t packet_handler, uint16_t cover_art_cid) { uint8_t buffer[20]; uint16_t size = hci_event_create_from_template_and_arguments(buffer, sizeof(buffer), &avrcp_cover_art_client_disconnected, cover_art_cid); (*packet_handler)(HCI_EVENT_PACKET, 0, buffer, size); } static void avrcp_cover_art_finalize_connection(avrcp_cover_art_client_t *cover_art_client) { btstack_assert(cover_art_client != NULL); memset(cover_art_client, 0, sizeof(avrcp_cover_art_client_t)); btstack_linked_list_remove(&avrcp_cover_art_client_connections, (btstack_linked_item_t *) cover_art_client); } static void avrcp_cover_art_client_parser_callback_connect(void * user_data, uint8_t header_id, uint16_t total_len, uint16_t data_offset, const uint8_t * data_buffer, uint16_t data_len){ avrcp_cover_art_client_t * client = (avrcp_cover_art_client_t *) user_data; switch (header_id){ case OBEX_HEADER_CONNECTION_ID: if (obex_parser_header_store(client->obex_header_buffer, sizeof(client->obex_header_buffer), total_len, data_offset, data_buffer, data_len) == OBEX_PARSER_HEADER_COMPLETE){ goep_client_set_connection_id(client->goep_cid, big_endian_read_32(client->obex_header_buffer, 0)); } break; default: break; } } static void avrcp_cover_art_client_prepare_srm_header(avrcp_cover_art_client_t * cover_art_client){ if (cover_art_client->flow_control_enabled == false){ goep_client_header_add_srm_enable(cover_art_client->goep_cid); cover_art_client->srm_state = SRM_W4_CONFIRM; } } static void obex_srm_init(avrcp_cover_art_obex_srm_t * obex_srm){ obex_srm->srm_value = OBEX_SRM_DISABLE; obex_srm->srmp_value = OBEX_SRMP_NEXT; } static void avrcp_cover_art_client_handle_srm_headers(avrcp_cover_art_client_t *context) { const avrcp_cover_art_obex_srm_t * obex_srm = &context->obex_srm; // Update SRM state based on SRM headers switch (context->srm_state){ case SRM_W4_CONFIRM: switch (obex_srm->srm_value){ case OBEX_SRM_ENABLE: switch (obex_srm->srmp_value){ case OBEX_SRMP_WAIT: context->srm_state = SRM_ENABLED_BUT_WAITING; break; default: context->srm_state = SRM_ENABLED; break; } break; default: context->srm_state = SRM_DISABLED; break; } break; case SRM_ENABLED_BUT_WAITING: switch (obex_srm->srmp_value){ case OBEX_SRMP_WAIT: context->srm_state = SRM_ENABLED_BUT_WAITING; break; default: context->srm_state = SRM_ENABLED; break; } break; default: break; } log_info("SRM state %u", context->srm_state); } static void avrcp_cover_art_client_parser_callback_get_operation(void * user_data, uint8_t header_id, uint16_t total_len, uint16_t data_offset, const uint8_t * data_buffer, uint16_t data_len){ avrcp_cover_art_client_t *client = (avrcp_cover_art_client_t *) user_data; switch (header_id) { case OBEX_HEADER_SINGLE_RESPONSE_MODE: obex_parser_header_store(&client->obex_srm.srm_value, 1, total_len, data_offset, data_buffer, data_len); break; case OBEX_HEADER_SINGLE_RESPONSE_MODE_PARAMETER: obex_parser_header_store(&client->obex_srm.srmp_value, 1, total_len, data_offset, data_buffer, data_len); break; case OBEX_HEADER_BODY: case OBEX_HEADER_END_OF_BODY: switch(client->state){ case AVRCP_COVER_ART_W4_OBJECT: client->packet_handler(BIP_DATA_PACKET, client->cover_art_cid, (uint8_t *) data_buffer, data_len); if (data_offset + data_len == total_len){ client->flow_wait_for_user = true; } break; default: btstack_unreachable(); break; } break; default: // ignore other headers break; } } static void avrcp_cover_art_client_prepare_get_operation(avrcp_cover_art_client_t * cover_art_client){ obex_parser_init_for_response(&cover_art_client->obex_parser, OBEX_OPCODE_GET, avrcp_cover_art_client_parser_callback_get_operation, cover_art_client); obex_srm_init(&cover_art_client->obex_srm); cover_art_client->obex_parser_waiting_for_response = true; } static void avrcp_cover_art_client_handle_can_send_now(avrcp_cover_art_client_t * cover_art_client){ switch (cover_art_client->state) { case AVRCP_COVER_ART_W2_SEND_CONNECT_REQUEST: // prepare request goep_client_request_create_connect(cover_art_client->goep_cid, OBEX_VERSION, 0, OBEX_MAX_PACKETLEN_DEFAULT); goep_client_header_add_target(cover_art_client->goep_cid, avrcp_cover_art_uuid, 16); // state cover_art_client->state = AVRCP_COVER_ART_W4_CONNECT_RESPONSE; // prepare response obex_parser_init_for_response(&cover_art_client->obex_parser, OBEX_OPCODE_CONNECT, avrcp_cover_art_client_parser_callback_connect, cover_art_client); obex_srm_init(&cover_art_client->obex_srm); cover_art_client->obex_parser_waiting_for_response = true; // send packet goep_client_execute(cover_art_client->goep_cid); break; case AVRCP_COVER_ART_W2_SEND_GET_OBJECT: goep_client_request_create_get(cover_art_client->goep_cid); if (cover_art_client->first_request){ cover_art_client->first_request = false; avrcp_cover_art_client_prepare_srm_header(cover_art_client); goep_client_header_add_type(cover_art_client->goep_cid, cover_art_client->object_type); if (cover_art_client->image_descriptor != NULL){ goep_client_header_add_variable(cover_art_client->goep_cid, OBEX_HEADER_IMG_DESCRIPTOR, (const uint8_t *) cover_art_client->image_descriptor, (uint16_t) strlen(cover_art_client->image_descriptor)); } uint8_t image_handle_len = btstack_max(7, (uint16_t) strlen(cover_art_client->image_handle)); goep_client_header_add_unicode_prefix(cover_art_client->goep_cid, OBEX_HEADER_IMG_HANDLE, cover_art_client->image_handle, image_handle_len); } // state cover_art_client->state = AVRCP_COVER_ART_W4_OBJECT; cover_art_client->flow_next_triggered = 0; cover_art_client->flow_wait_for_user = 0; // prepare response avrcp_cover_art_client_prepare_get_operation(cover_art_client); // send packet goep_client_execute(cover_art_client->goep_cid); break; case AVRCP_COVER_ART_W2_SEND_DISCONNECT_REQUEST: // prepare request goep_client_request_create_disconnect(cover_art_client->goep_cid); // state cover_art_client->state = AVRCP_COVER_ART_W4_DISCONNECT_RESPONSE; // prepare response obex_parser_init_for_response(&cover_art_client->obex_parser, OBEX_OPCODE_DISCONNECT, NULL, cover_art_client); cover_art_client->obex_parser_waiting_for_response = true; // send packet goep_client_execute(cover_art_client->goep_cid); return; default: break; } } static void avrcp_cover_art_goep_event_handler(const uint8_t *packet, uint16_t size) { UNUSED(size); uint8_t status; avrcp_cover_art_client_t * cover_art_client; btstack_packet_handler_t packet_handler; uint16_t cover_art_cid; switch (hci_event_packet_get_type(packet)) { case HCI_EVENT_GOEP_META: switch (hci_event_goep_meta_get_subevent_code(packet)){ case GOEP_SUBEVENT_CONNECTION_OPENED: cover_art_client = avrcp_cover_art_client_for_goep_cid(goep_subevent_connection_opened_get_goep_cid(packet)); btstack_assert(cover_art_client != NULL); status = goep_subevent_connection_opened_get_status(packet); if (status){ log_info("connection failed %u", status); avrcp_cover_art_finalize_connection(cover_art_client); avrcp_cover_art_client_emit_connection_established(cover_art_client->packet_handler, status, cover_art_client->addr, cover_art_client->avrcp_cid, cover_art_client->cover_art_cid); } else { log_info("connection established"); cover_art_client->state = AVRCP_COVER_ART_W2_SEND_CONNECT_REQUEST; goep_client_request_can_send_now(cover_art_client->goep_cid); } break; case GOEP_SUBEVENT_CONNECTION_CLOSED: cover_art_client = avrcp_cover_art_client_for_goep_cid(goep_subevent_connection_opened_get_goep_cid(packet)); btstack_assert(cover_art_client != NULL); if (cover_art_client->state > AVRCP_COVER_ART_CONNECTED){ cover_art_client_emit_operation_complete_event(cover_art_client, OBEX_DISCONNECTED); } cover_art_cid = cover_art_client->cover_art_cid; packet_handler = cover_art_client->packet_handler; avrcp_cover_art_finalize_connection(cover_art_client); avrcp_cover_art_client_emit_connection_released(packet_handler, cover_art_cid); break; case GOEP_SUBEVENT_CAN_SEND_NOW: cover_art_client = avrcp_cover_art_client_for_goep_cid(goep_subevent_can_send_now_get_goep_cid(packet)); btstack_assert(cover_art_client != NULL); avrcp_cover_art_client_handle_can_send_now(cover_art_client); break; default: break; } break; default: break; } } static void avrcp_cover_art_client_goep_data_handler(avrcp_cover_art_client_t * cover_art_client, uint8_t *packet, uint16_t size){ btstack_assert(cover_art_client->obex_parser_waiting_for_response); obex_parser_object_state_t parser_state; parser_state = obex_parser_process_data(&cover_art_client->obex_parser, packet, size); if (parser_state == OBEX_PARSER_OBJECT_STATE_COMPLETE){ cover_art_client->obex_parser_waiting_for_response = false; obex_parser_operation_info_t op_info; obex_parser_get_operation_info(&cover_art_client->obex_parser, &op_info); switch (cover_art_client->state){ case AVRCP_COVER_ART_W4_CONNECT_RESPONSE: switch (op_info.response_code) { case OBEX_RESP_SUCCESS: cover_art_client->state = AVRCP_COVER_ART_CONNECTED; avrcp_cover_art_client_emit_connection_established(cover_art_client->packet_handler, ERROR_CODE_SUCCESS, cover_art_client->addr, cover_art_client->avrcp_cid, cover_art_client->cover_art_cid); break; default: log_info("pbap: obex connect failed, result 0x%02x", packet[0]); cover_art_client->state = AVRCP_COVER_ART_INIT; avrcp_cover_art_client_emit_connection_established(cover_art_client->packet_handler, OBEX_CONNECT_FAILED, cover_art_client->addr, cover_art_client->avrcp_cid, cover_art_client->cover_art_cid); break; } break; case AVRCP_COVER_ART_W4_OBJECT: switch (op_info.response_code) { case OBEX_RESP_CONTINUE: avrcp_cover_art_client_handle_srm_headers(cover_art_client); if (cover_art_client->srm_state == SRM_ENABLED) { // prepare response avrcp_cover_art_client_prepare_get_operation(cover_art_client); break; } cover_art_client->state = AVRCP_COVER_ART_W2_SEND_GET_OBJECT; if (!cover_art_client->flow_control_enabled || !cover_art_client->flow_wait_for_user || cover_art_client->flow_next_triggered) { goep_client_request_can_send_now(cover_art_client->goep_cid); } break; case OBEX_RESP_SUCCESS: cover_art_client->state = AVRCP_COVER_ART_CONNECTED; cover_art_client_emit_operation_complete_event(cover_art_client, ERROR_CODE_SUCCESS); break; default: log_info("unexpected response 0x%02x", packet[0]); cover_art_client->state = AVRCP_COVER_ART_CONNECTED; cover_art_client_emit_operation_complete_event(cover_art_client, OBEX_UNKNOWN_ERROR); break; } break; case AVRCP_COVER_ART_W4_DISCONNECT_RESPONSE: goep_client_disconnect(cover_art_client->goep_cid); break; default: btstack_unreachable(); break; } } } static void avrcp_cover_art_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){ UNUSED(channel); // ok: there is no channel UNUSED(size); // ok: handling own goep events avrcp_cover_art_client_t * cover_art_client; switch (packet_type){ case HCI_EVENT_PACKET: avrcp_cover_art_goep_event_handler(packet, size); break; case GOEP_DATA_PACKET: cover_art_client = avrcp_cover_art_client_for_goep_cid(channel); btstack_assert(cover_art_client != NULL); avrcp_cover_art_client_goep_data_handler(cover_art_client, packet, size); break; default: break; } } static uint8_t avrcp_cover_art_client_setup_connection(avrcp_cover_art_client_t * cover_art_client, uint16_t l2cap_psm){ cover_art_client->state = AVRCP_COVER_ART_W4_GOEP_CONNECTION; return goep_client_connect_l2cap(&cover_art_client->goep_client, (l2cap_ertm_config_t *) cover_art_client->ertm_config, cover_art_client->ertm_buffer, cover_art_client->ertm_buffer_size, &avrcp_cover_art_packet_handler, cover_art_client->addr, l2cap_psm, &cover_art_client->goep_cid); } static void avrcp_cover_art_handle_sdp_query_complete(avrcp_connection_t * connection, uint8_t status){ avrcp_cover_art_client_t * cover_art_client = avrcp_cover_art_client_for_avrcp_cid(connection->avrcp_cid); if (cover_art_client == NULL) { return; } if (cover_art_client->state != AVRCP_COVER_ART_W4_SDP_QUERY_COMPLETE){ return; } // l2cap available? if (status == ERROR_CODE_SUCCESS){ if (connection->cover_art_psm == 0){ status = SDP_SERVICE_NOT_FOUND; } } if (status == ERROR_CODE_SUCCESS) { // ready to connect cover_art_client->state = AVRCP_COVER_ART_W2_GOEP_CONNECT; status = avrcp_cover_art_client_setup_connection(cover_art_client, connection->cover_art_psm); } if (status != ERROR_CODE_SUCCESS){ btstack_packet_handler_t packet_handler = cover_art_client->packet_handler; uint16_t cover_art_cid = cover_art_client->cover_art_cid; uint16_t avrcp_cid = cover_art_client->avrcp_cid; avrcp_cover_art_finalize_connection(cover_art_client); avrcp_cover_art_client_emit_connection_established(packet_handler, status, connection->remote_addr, avrcp_cid, cover_art_cid); } } void avrcp_cover_art_client_init(void){ avrcp_register_cover_art_sdp_query_complete_handler(&avrcp_cover_art_handle_sdp_query_complete); } uint8_t avrcp_cover_art_client_connect(avrcp_cover_art_client_t *cover_art_client, btstack_packet_handler_t packet_handler, bd_addr_t remote_addr, uint8_t *ertm_buffer, uint32_t ertm_buffer_size, const l2cap_ertm_config_t *ertm_config, uint16_t *avrcp_cover_art_cid) { avrcp_connection_t * connection_controller = avrcp_get_connection_for_bd_addr_for_role(AVRCP_CONTROLLER, remote_addr); avrcp_connection_t * connection_target = avrcp_get_connection_for_bd_addr_for_role(AVRCP_TARGET, remote_addr); if ((connection_target == NULL) || (connection_controller == NULL)){ return ERROR_CODE_COMMAND_DISALLOWED; } cover_art_client->cover_art_cid = avrcp_cover_art_client_next_cid(); memcpy(cover_art_client->addr, remote_addr, 6); cover_art_client->avrcp_cid = connection_controller->avrcp_cid; cover_art_client->packet_handler = packet_handler; cover_art_client->flow_control_enabled = false; // store ERTM config cover_art_client->ertm_config = ertm_config; cover_art_client->ertm_buffer = ertm_buffer; cover_art_client->ertm_buffer_size = ertm_buffer_size; if (avrcp_cover_art_cid != NULL){ *avrcp_cover_art_cid = cover_art_client->cover_art_cid; } btstack_linked_list_add(&avrcp_cover_art_client_connections, (btstack_linked_item_t *) cover_art_client); if (connection_controller->cover_art_psm == 0){ cover_art_client->state = AVRCP_COVER_ART_W4_SDP_QUERY_COMPLETE; avrcp_trigger_sdp_query(connection_controller, connection_controller); return ERROR_CODE_SUCCESS; } else { return avrcp_cover_art_client_setup_connection(cover_art_client, connection_controller->cover_art_psm); } } static uint8_t avrcp_cover_art_client_get_object(uint16_t avrcp_cover_art_cid, const char * object_type, const char * image_handle, const char * image_descriptor){ avrcp_cover_art_client_t * cover_art_client = avrcp_cover_art_client_for_cover_art_cid(avrcp_cover_art_cid); if (cover_art_client == NULL){ return ERROR_CODE_UNKNOWN_CONNECTION_IDENTIFIER; } if (cover_art_client->state != AVRCP_COVER_ART_CONNECTED){ return ERROR_CODE_COMMAND_DISALLOWED; } cover_art_client->state = AVRCP_COVER_ART_W2_SEND_GET_OBJECT; cover_art_client->first_request = true; cover_art_client->image_handle = image_handle; cover_art_client->image_descriptor = image_descriptor; cover_art_client->object_type = object_type; goep_client_request_can_send_now(cover_art_client->goep_cid); return ERROR_CODE_SUCCESS; } uint8_t avrcp_cover_art_client_get_linked_thumbnail(uint16_t avrcp_cover_art_cid, const char * image_handle){ return avrcp_cover_art_client_get_object(avrcp_cover_art_cid, avrcp_cover_art_linked_thumbnail_type, image_handle, NULL); } uint8_t avrcp_cover_art_client_get_image(uint16_t avrcp_cover_art_cid, const char * image_handle, const char * image_descriptor){ return avrcp_cover_art_client_get_object(avrcp_cover_art_cid, avrcp_cover_art_image_type, image_handle, image_descriptor); } uint8_t avrcp_cover_art_client_get_image_properties(uint16_t avrcp_cover_art_cid, const char * image_handle){ return avrcp_cover_art_client_get_object(avrcp_cover_art_cid, avrcp_cover_art_image_properties_type, image_handle, NULL); } uint8_t avrcp_cover_art_client_disconnect(uint16_t avrcp_cover_art_cid){ avrcp_cover_art_client_t * cover_art_client = avrcp_cover_art_client_for_cover_art_cid(avrcp_cover_art_cid); if (cover_art_client == NULL){ return ERROR_CODE_UNKNOWN_CONNECTION_IDENTIFIER; } if (cover_art_client->state < AVRCP_COVER_ART_CONNECTED){ return ERROR_CODE_COMMAND_DISALLOWED; } cover_art_client->state = AVRCP_COVER_ART_W2_SEND_DISCONNECT_REQUEST; goep_client_request_can_send_now(cover_art_client->goep_cid); return ERROR_CODE_SUCCESS; } void avrcp_cover_art_client_deinit(void){ avrcp_cover_art_client_connections = NULL; } #endif /* ENABLE_AVRCP_COVER_ART */