You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
15 KiB
460 lines
15 KiB
/****************************************************************************** |
|
* |
|
* Copyright (C) 2014 Google, Inc. |
|
* |
|
* 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. |
|
* |
|
******************************************************************************/ |
|
|
|
#define LOG_TAG "bt_l2cap_client" |
|
|
|
#include "stack/include/l2cap_client.h" |
|
|
|
#include <base/logging.h> |
|
#include <string.h> |
|
|
|
#include "osi/include/allocator.h" |
|
#include "osi/include/buffer.h" |
|
#include "osi/include/list.h" |
|
#include "osi/include/log.h" |
|
#include "osi/include/osi.h" |
|
#include "stack/include/l2c_api.h" |
|
|
|
struct l2cap_client_t { |
|
l2cap_client_callbacks_t callbacks; |
|
void* context; |
|
|
|
uint16_t local_channel_id; |
|
uint16_t remote_mtu; |
|
bool configured_self; |
|
bool configured_peer; |
|
bool is_congested; |
|
list_t* outbound_fragments; |
|
}; |
|
|
|
static void connect_completed_cb(uint16_t local_channel_id, |
|
uint16_t error_code); |
|
static void config_request_cb(uint16_t local_channel_id, |
|
tL2CAP_CFG_INFO* requested_parameters); |
|
static void config_completed_cb(uint16_t local_channel_id, |
|
tL2CAP_CFG_INFO* negotiated_parameters); |
|
static void disconnect_request_cb(uint16_t local_channel_id, bool ack_required); |
|
static void disconnect_completed_cb(uint16_t local_channel_id, |
|
uint16_t error_code); |
|
static void congestion_cb(uint16_t local_channel_id, bool is_congested); |
|
static void read_ready_cb(uint16_t local_channel_id, BT_HDR* packet); |
|
static void write_completed_cb(uint16_t local_channel_id, |
|
uint16_t packets_completed); |
|
|
|
static void fragment_packet(l2cap_client_t* client, buffer_t* packet); |
|
static void dispatch_fragments(l2cap_client_t* client); |
|
static l2cap_client_t* find(uint16_t local_channel_id); |
|
|
|
// From the Bluetooth Core specification. |
|
static const uint16_t L2CAP_MTU_DEFAULT = 672; |
|
static const uint16_t L2CAP_MTU_MINIMUM = 48; |
|
|
|
static const tL2CAP_APPL_INFO l2cap_callbacks = { |
|
.pL2CA_ConnectCfm_Cb = connect_completed_cb, |
|
.pL2CA_ConfigInd_Cb = config_request_cb, |
|
.pL2CA_ConfigCfm_Cb = config_completed_cb, |
|
.pL2CA_DisconnectInd_Cb = disconnect_request_cb, |
|
.pL2CA_DisconnectCfm_Cb = disconnect_completed_cb, |
|
.pL2CA_CongestionStatus_Cb = congestion_cb, |
|
.pL2CA_DataInd_Cb = read_ready_cb, |
|
.pL2CA_TxComplete_Cb = write_completed_cb, |
|
}; |
|
|
|
static list_t* |
|
l2cap_clients; // A list of l2cap_client_t. Container does not own objects. |
|
|
|
buffer_t* l2cap_buffer_new(size_t size) { |
|
buffer_t* buf = buffer_new(size + L2CAP_MIN_OFFSET); |
|
buffer_t* slice = NULL; |
|
if (buf) slice = buffer_new_slice(buf, size); |
|
buffer_free(buf); |
|
return slice; |
|
} |
|
|
|
l2cap_client_t* l2cap_client_new(const l2cap_client_callbacks_t* callbacks, |
|
void* context) { |
|
CHECK(callbacks != NULL); |
|
CHECK(callbacks->connected != NULL); |
|
CHECK(callbacks->disconnected != NULL); |
|
CHECK(callbacks->read_ready != NULL); |
|
CHECK(callbacks->write_ready != NULL); |
|
|
|
if (!l2cap_clients) { |
|
l2cap_clients = list_new(NULL); |
|
if (!l2cap_clients) { |
|
LOG_ERROR(LOG_TAG, "%s unable to allocate space for L2CAP client list.", |
|
__func__); |
|
return NULL; |
|
} |
|
} |
|
|
|
l2cap_client_t* ret = (l2cap_client_t*)osi_calloc(sizeof(l2cap_client_t)); |
|
|
|
ret->callbacks = *callbacks; |
|
ret->context = context; |
|
|
|
ret->remote_mtu = L2CAP_MTU_DEFAULT; |
|
ret->outbound_fragments = list_new(NULL); |
|
|
|
list_append(l2cap_clients, ret); |
|
|
|
return ret; |
|
} |
|
|
|
void l2cap_client_free(l2cap_client_t* client) { |
|
if (!client) return; |
|
|
|
list_remove(l2cap_clients, client); |
|
l2cap_client_disconnect(client); |
|
list_free(client->outbound_fragments); |
|
osi_free(client); |
|
} |
|
|
|
bool l2cap_client_connect(l2cap_client_t* client, |
|
const RawAddress& remote_bdaddr, uint16_t psm) { |
|
CHECK(client != NULL); |
|
CHECK(psm != 0); |
|
CHECK(!remote_bdaddr.IsEmpty()); |
|
CHECK(client->local_channel_id == 0); |
|
CHECK(!client->configured_self); |
|
CHECK(!client->configured_peer); |
|
CHECK(!L2C_INVALID_PSM(psm)); |
|
|
|
client->local_channel_id = L2CA_ConnectReq(psm, remote_bdaddr); |
|
if (!client->local_channel_id) { |
|
LOG_ERROR(LOG_TAG, "%s unable to create L2CAP connection.", __func__); |
|
return false; |
|
} |
|
|
|
L2CA_SetConnectionCallbacks(client->local_channel_id, &l2cap_callbacks); |
|
return true; |
|
} |
|
|
|
void l2cap_client_disconnect(l2cap_client_t* client) { |
|
CHECK(client != NULL); |
|
|
|
if (client->local_channel_id && !L2CA_DisconnectReq(client->local_channel_id)) |
|
LOG_ERROR(LOG_TAG, "%s unable to send disconnect message for LCID 0x%04x.", |
|
__func__, client->local_channel_id); |
|
|
|
client->local_channel_id = 0; |
|
client->remote_mtu = L2CAP_MTU_DEFAULT; |
|
client->configured_self = false; |
|
client->configured_peer = false; |
|
client->is_congested = false; |
|
|
|
for (const list_node_t* node = list_begin(client->outbound_fragments); |
|
node != list_end(client->outbound_fragments); node = list_next(node)) |
|
osi_free(list_node(node)); |
|
|
|
list_clear(client->outbound_fragments); |
|
} |
|
|
|
bool l2cap_client_is_connected(const l2cap_client_t* client) { |
|
CHECK(client != NULL); |
|
|
|
return client->local_channel_id != 0 && client->configured_self && |
|
client->configured_peer; |
|
} |
|
|
|
bool l2cap_client_write(l2cap_client_t* client, buffer_t* packet) { |
|
CHECK(client != NULL); |
|
CHECK(packet != NULL); |
|
CHECK(l2cap_client_is_connected(client)); |
|
|
|
if (client->is_congested) return false; |
|
|
|
fragment_packet(client, packet); |
|
dispatch_fragments(client); |
|
return true; |
|
} |
|
|
|
static void connect_completed_cb(uint16_t local_channel_id, |
|
uint16_t error_code) { |
|
CHECK(local_channel_id != 0); |
|
|
|
l2cap_client_t* client = find(local_channel_id); |
|
if (!client) { |
|
LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client for LCID 0x%04x.", |
|
__func__, local_channel_id); |
|
return; |
|
} |
|
|
|
if (error_code != L2CAP_CONN_OK) { |
|
LOG_ERROR(LOG_TAG, "%s error connecting L2CAP channel: %d.", __func__, |
|
error_code); |
|
client->callbacks.disconnected(client, client->context); |
|
return; |
|
} |
|
|
|
// Use default L2CAP parameters. |
|
tL2CAP_CFG_INFO desired_parameters; |
|
memset(&desired_parameters, 0, sizeof(desired_parameters)); |
|
if (!L2CA_ConfigReq(local_channel_id, &desired_parameters)) { |
|
LOG_ERROR(LOG_TAG, "%s error sending L2CAP config parameters.", __func__); |
|
client->callbacks.disconnected(client, client->context); |
|
} |
|
} |
|
|
|
static void config_request_cb(uint16_t local_channel_id, |
|
tL2CAP_CFG_INFO* requested_parameters) { |
|
tL2CAP_CFG_INFO response; |
|
l2cap_client_t* client = find(local_channel_id); |
|
|
|
if (!client) { |
|
LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client matching LCID 0x%04x.", |
|
__func__, local_channel_id); |
|
return; |
|
} |
|
|
|
memset(&response, 0, sizeof(response)); |
|
response.result = L2CAP_CFG_OK; |
|
|
|
if (requested_parameters->mtu_present) { |
|
// Make sure the peer chose an MTU at least as large as the minimum L2CAP |
|
// MTU defined by the Bluetooth Core spec. |
|
if (requested_parameters->mtu < L2CAP_MTU_MINIMUM) { |
|
response.mtu = L2CAP_MTU_MINIMUM; |
|
response.mtu_present = true; |
|
response.result = L2CAP_CFG_UNACCEPTABLE_PARAMS; |
|
} else { |
|
client->remote_mtu = requested_parameters->mtu; |
|
} |
|
} |
|
|
|
if (requested_parameters->fcr_present) { |
|
if (requested_parameters->fcr.mode != L2CAP_FCR_BASIC_MODE) { |
|
response.fcr_present = true; |
|
response.fcr = requested_parameters->fcr; |
|
response.fcr.mode = L2CAP_FCR_BASIC_MODE; |
|
response.result = L2CAP_CFG_UNACCEPTABLE_PARAMS; |
|
} |
|
} |
|
|
|
if (!L2CA_ConfigRsp(local_channel_id, &response)) { |
|
LOG_ERROR(LOG_TAG, "%s unable to send config response for LCID 0x%04x.", |
|
__func__, local_channel_id); |
|
l2cap_client_disconnect(client); |
|
return; |
|
} |
|
|
|
// If we've configured both endpoints, let the listener know we've connected. |
|
client->configured_peer = true; |
|
if (l2cap_client_is_connected(client)) |
|
client->callbacks.connected(client, client->context); |
|
} |
|
|
|
static void config_completed_cb(uint16_t local_channel_id, |
|
tL2CAP_CFG_INFO* negotiated_parameters) { |
|
l2cap_client_t* client = find(local_channel_id); |
|
|
|
if (!client) { |
|
LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client matching LCID 0x%04x.", |
|
__func__, local_channel_id); |
|
return; |
|
} |
|
|
|
switch (negotiated_parameters->result) { |
|
// We'll get another configuration response later. |
|
case L2CAP_CFG_PENDING: |
|
break; |
|
|
|
case L2CAP_CFG_UNACCEPTABLE_PARAMS: |
|
// TODO: see if we can renegotiate parameters instead of dropping the |
|
// connection. |
|
LOG_WARN( |
|
LOG_TAG, |
|
"%s dropping L2CAP connection due to unacceptable config parameters.", |
|
__func__); |
|
l2cap_client_disconnect(client); |
|
break; |
|
|
|
case L2CAP_CFG_OK: |
|
// If we've configured both endpoints, let the listener know we've |
|
// connected. |
|
client->configured_self = true; |
|
if (l2cap_client_is_connected(client)) |
|
client->callbacks.connected(client, client->context); |
|
break; |
|
|
|
// Failure, no further parameter negotiation possible. |
|
default: |
|
LOG_WARN(LOG_TAG, |
|
"%s L2CAP parameter negotiation failed with error code %d.", |
|
__func__, negotiated_parameters->result); |
|
l2cap_client_disconnect(client); |
|
break; |
|
} |
|
} |
|
|
|
static void disconnect_request_cb(uint16_t local_channel_id, |
|
bool ack_required) { |
|
l2cap_client_t* client = find(local_channel_id); |
|
if (!client) { |
|
LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client with LCID 0x%04x.", |
|
__func__, local_channel_id); |
|
return; |
|
} |
|
|
|
if (ack_required) L2CA_DisconnectRsp(local_channel_id); |
|
|
|
// We already sent a disconnect response so this LCID is now invalid. |
|
client->local_channel_id = 0; |
|
l2cap_client_disconnect(client); |
|
|
|
client->callbacks.disconnected(client, client->context); |
|
} |
|
|
|
static void disconnect_completed_cb(uint16_t local_channel_id, |
|
UNUSED_ATTR uint16_t error_code) { |
|
CHECK(local_channel_id != 0); |
|
|
|
l2cap_client_t* client = find(local_channel_id); |
|
if (!client) { |
|
LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client with LCID 0x%04x.", |
|
__func__, local_channel_id); |
|
return; |
|
} |
|
|
|
client->local_channel_id = 0; |
|
l2cap_client_disconnect(client); |
|
|
|
client->callbacks.disconnected(client, client->context); |
|
} |
|
|
|
static void congestion_cb(uint16_t local_channel_id, bool is_congested) { |
|
CHECK(local_channel_id != 0); |
|
|
|
l2cap_client_t* client = find(local_channel_id); |
|
if (!client) { |
|
LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client matching LCID 0x%04x.", |
|
__func__, local_channel_id); |
|
return; |
|
} |
|
|
|
client->is_congested = is_congested; |
|
|
|
if (!is_congested) { |
|
// If we just decongested, dispatch whatever we have left over in our queue. |
|
// Once that's done, if we're still decongested, notify the listener so it |
|
// can start writing again. |
|
dispatch_fragments(client); |
|
if (!client->is_congested) |
|
client->callbacks.write_ready(client, client->context); |
|
} |
|
} |
|
|
|
static void read_ready_cb(uint16_t local_channel_id, BT_HDR* packet) { |
|
CHECK(local_channel_id != 0); |
|
|
|
l2cap_client_t* client = find(local_channel_id); |
|
if (!client) { |
|
LOG_ERROR(LOG_TAG, "%s unable to find L2CAP client matching LCID 0x%04x.", |
|
__func__, local_channel_id); |
|
return; |
|
} |
|
|
|
// TODO(sharvil): eliminate copy from BT_HDR. |
|
buffer_t* buffer = buffer_new(packet->len); |
|
memcpy(buffer_ptr(buffer), packet->data + packet->offset, packet->len); |
|
osi_free(packet); |
|
|
|
client->callbacks.read_ready(client, buffer, client->context); |
|
buffer_free(buffer); |
|
} |
|
|
|
static void write_completed_cb(UNUSED_ATTR uint16_t local_channel_id, |
|
UNUSED_ATTR uint16_t packets_completed) { |
|
// Do nothing. We update congestion state based on the congestion callback |
|
// and we've already removed items from outbound_fragments list so we don't |
|
// really care how many packets were successfully dispatched. |
|
} |
|
|
|
static void fragment_packet(l2cap_client_t* client, buffer_t* packet) { |
|
CHECK(client != NULL); |
|
CHECK(packet != NULL); |
|
|
|
// TODO(sharvil): eliminate copy into BT_HDR. |
|
BT_HDR* bt_packet = static_cast<BT_HDR*>( |
|
osi_malloc(buffer_length(packet) + L2CAP_MIN_OFFSET + sizeof(BT_HDR))); |
|
bt_packet->offset = L2CAP_MIN_OFFSET; |
|
bt_packet->len = buffer_length(packet); |
|
memcpy(bt_packet->data + bt_packet->offset, buffer_ptr(packet), |
|
buffer_length(packet)); |
|
|
|
for (;;) { |
|
if (bt_packet->len <= client->remote_mtu) { |
|
if (bt_packet->len > 0) |
|
list_append(client->outbound_fragments, bt_packet); |
|
else |
|
osi_free(bt_packet); |
|
break; |
|
} |
|
|
|
BT_HDR* fragment = static_cast<BT_HDR*>( |
|
osi_malloc(client->remote_mtu + L2CAP_MIN_OFFSET + sizeof(BT_HDR))); |
|
fragment->offset = L2CAP_MIN_OFFSET; |
|
fragment->len = client->remote_mtu; |
|
memcpy(fragment->data + fragment->offset, |
|
bt_packet->data + bt_packet->offset, client->remote_mtu); |
|
|
|
list_append(client->outbound_fragments, fragment); |
|
|
|
bt_packet->offset += client->remote_mtu; |
|
bt_packet->len -= client->remote_mtu; |
|
} |
|
} |
|
|
|
static void dispatch_fragments(l2cap_client_t* client) { |
|
CHECK(client != NULL); |
|
CHECK(!client->is_congested); |
|
|
|
while (!list_is_empty(client->outbound_fragments)) { |
|
BT_HDR* packet = (BT_HDR*)list_front(client->outbound_fragments); |
|
list_remove(client->outbound_fragments, packet); |
|
|
|
switch (L2CA_DataWrite(client->local_channel_id, packet)) { |
|
case L2CAP_DW_CONGESTED: |
|
client->is_congested = true; |
|
return; |
|
|
|
case L2CAP_DW_FAILED: |
|
LOG_ERROR(LOG_TAG, |
|
"%s error writing data to L2CAP connection LCID 0x%04x; " |
|
"disconnecting.", |
|
__func__, client->local_channel_id); |
|
l2cap_client_disconnect(client); |
|
return; |
|
|
|
case L2CAP_DW_SUCCESS: |
|
break; |
|
} |
|
} |
|
} |
|
|
|
static l2cap_client_t* find(uint16_t local_channel_id) { |
|
CHECK(local_channel_id != 0); |
|
|
|
for (const list_node_t* node = list_begin(l2cap_clients); |
|
node != list_end(l2cap_clients); node = list_next(node)) { |
|
l2cap_client_t* client = (l2cap_client_t*)list_node(node); |
|
if (client->local_channel_id == local_channel_id) return client; |
|
} |
|
|
|
return NULL; |
|
}
|
|
|