/*
*
* BlueZ - Bluetooth protocol stack for Linux
*
* Copyright (C) 2000-2002 Maxim Krasnyansky <maxk@qualcomm.com>
* Copyright (C) 2003-2011 Marcel Holtmann <marcel@holtmann.org>
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/l2cap.h>
#include "parser.h"
#include "sdp.h"
typedef struct {
uint16_t handle;
struct frame frm;
} handle_info;
#define HANDLE_TABLE_SIZE 10
static handle_info handle_table[HANDLE_TABLE_SIZE];
typedef struct {
uint16_t handle;
uint16_t cid;
uint16_t psm;
uint16_t num;
uint8_t mode;
} cid_info;
#define CID_TABLE_SIZE 20
static cid_info cid_table[2][CID_TABLE_SIZE];
#define SCID cid_table[0]
#define DCID cid_table[1]
static struct frame *add_handle(uint16_t handle)
{
register handle_info *t = handle_table;
register int i;
for (i = 0; i < HANDLE_TABLE_SIZE; i++)
if (!t[i].handle) {
t[i].handle = handle;
return &t[i].frm;
}
return NULL;
}
static struct frame *get_frame(uint16_t handle)
{
register handle_info *t = handle_table;
register int i;
for (i = 0; i < HANDLE_TABLE_SIZE; i++)
if (t[i].handle == handle)
return &t[i].frm;
return add_handle(handle);
}
static void add_cid(int in, uint16_t handle, uint16_t cid, uint16_t psm)
{
register cid_info *table = cid_table[in];
register int i, pos = -1;
uint16_t num = 1;
for (i = 0; i < CID_TABLE_SIZE; i++) {
if ((pos < 0 && !table[i].cid) || table[i].cid == cid)
pos = i;
if (table[i].psm == psm)
num++;
}
if (pos >= 0) {
table[pos].handle = handle;
table[pos].cid = cid;
table[pos].psm = psm;
table[pos].num = num;
table[pos].mode = 0;
}
}
static void del_cid(int in, uint16_t dcid, uint16_t scid)
{
register int t, i;
uint16_t cid[2];
if (!in) {
cid[0] = dcid;
cid[1] = scid;
} else {
cid[0] = scid;
cid[1] = dcid;
}
for (t = 0; t < 2; t++) {
for (i = 0; i < CID_TABLE_SIZE; i++)
if (cid_table[t][i].cid == cid[t]) {
cid_table[t][i].handle = 0;
cid_table[t][i].cid = 0;
cid_table[t][i].psm = 0;
cid_table[t][i].num = 0;
cid_table[t][i].mode = 0;
break;
}
}
}
static void del_handle(uint16_t handle)
{
register int t, i;
for (t = 0; t < 2; t++) {
for (i = 0; i < CID_TABLE_SIZE; i++)
if (cid_table[t][i].handle == handle) {
cid_table[t][i].handle = 0;
cid_table[t][i].cid = 0;
cid_table[t][i].psm = 0;
cid_table[t][i].num = 0;
cid_table[t][i].mode = 0;
break;
}
}
}
static uint16_t get_psm(int in, uint16_t cid)
{
register cid_info *table = cid_table[in];
register int i;
for (i = 0; i < CID_TABLE_SIZE; i++)
if (table[i].cid == cid)
return table[i].psm;
return parser.defpsm;
}
static uint16_t get_num(int in, uint16_t cid)
{
register cid_info *table = cid_table[in];
register int i;
for (i = 0; i < CID_TABLE_SIZE; i++)
if (table[i].cid == cid)
return table[i].num;
return 0;
}
static void set_mode(int in, uint16_t cid, uint8_t mode)
{
register cid_info *table = cid_table[in];
register int i;
for (i = 0; i < CID_TABLE_SIZE; i++)
if (table[i].cid == cid)
table[i].mode = mode;
}
static uint8_t get_mode(int in, uint16_t cid)
{
register cid_info *table = cid_table[in];
register int i;
for (i = 0; i < CID_TABLE_SIZE; i++)
if (table[i].cid == cid)
return table[i].mode;
return 0;
}
static uint32_t get_val(uint8_t *ptr, uint8_t len)
{
switch (len) {
case 1:
return *ptr;
case 2:
return btohs(bt_get_unaligned((uint16_t *) ptr));
case 4:
return btohl(bt_get_unaligned((uint32_t *) ptr));
}
return 0;
}
static char *reason2str(uint16_t reason)
{
switch (reason) {
case 0x0000:
return "Command not understood";
case 0x0001:
return "Signalling MTU exceeded";
case 0x0002:
return "Invalid CID in request";
default:
return "Reserved";
}
}
static char *connresult2str(uint16_t result)
{
switch (result) {
case 0x0000:
return "Connection successful";
case 0x0001:
return "Connection pending";
case 0x0002:
return "Connection refused - PSM not supported";
case 0x0003:
return "Connection refused - security block";
case 0x0004:
return "Connection refused - no resources available";
default:
return "Reserved";
}
}
static char *status2str(uint16_t status)
{
switch (status) {
case 0x0000:
return "No futher information available";
case 0x0001:
return "Authentication pending";
case 0x0002:
return "Authorization pending";
default:
return "Reserved";
}
}
static char *confresult2str(uint16_t result)
{
switch (result) {
case 0x0000:
return "Success";
case 0x0001:
return "Failure - unacceptable parameters";
case 0x0002:
return "Failure - rejected (no reason provided)";
case 0x0003:
return "Failure - unknown options";
default:
return "Reserved";
}
}
static char *inforesult2str(uint16_t result)
{
switch (result) {
case 0x0000:
return "Success";
case 0x0001:
return "Not supported";
default:
return "Reserved";
}
}
static char *type2str(uint8_t type)
{
switch (type) {
case 0x00:
return "No traffic";
case 0x01:
return "Best effort";
case 0x02:
return "Guaranteed";
default:
return "Reserved";
}
}
static char *mode2str(uint8_t mode)
{
switch (mode) {
case 0x00:
return "Basic";
case 0x01:
return "Retransmission";
case 0x02:
return "Flow control";
case 0x03:
return "Enhanced Retransmission";
case 0x04:
return "Streaming";
default:
return "Reserved";
}
}
static char *fcs2str(uint8_t fcs)
{
switch (fcs) {
case 0x00:
return "No FCS";
case 0x01:
return "CRC16 Check";
default:
return "Reserved";
}
}
static char *sar2str(uint8_t sar)
{
switch (sar) {
case 0x00:
return "Unsegmented";
case 0x01:
return "Start";
case 0x02:
return "End";
case 0x03:
return "Continuation";
default:
return "Bad SAR";
}
}
static char *supervisory2str(uint8_t supervisory)
{
switch (supervisory) {
case 0x00:
return "Receiver Ready (RR)";
case 0x01:
return "Reject (REJ)";
case 0x02:
return "Receiver Not Ready (RNR)";
case 0x03:
return "Select Reject (SREJ)";
default:
return "Bad Supervisory";
}
}
static inline void command_rej(int level, struct frame *frm)
{
l2cap_cmd_rej *h = frm->ptr;
uint16_t reason = btohs(h->reason);
uint32_t cid;
printf("Command rej: reason %d", reason);
switch (reason) {
case 0x0001:
printf(" mtu %d\n", get_val(frm->ptr + L2CAP_CMD_REJ_SIZE, 2));
break;
case 0x0002:
cid = get_val(frm->ptr + L2CAP_CMD_REJ_SIZE, 4);
printf(" dcid 0x%4.4x scid 0x%4.4x\n", cid & 0xffff, cid >> 16);
break;
default:
printf("\n");
break;
}
p_indent(level + 1, frm);
printf("%s\n", reason2str(reason));
}
static inline void conn_req(int level, struct frame *frm)
{
l2cap_conn_req *h = frm->ptr;
uint16_t psm = btohs(h->psm);
uint16_t scid = btohs(h->scid);
add_cid(frm->in, frm->handle, scid, psm);
if (p_filter(FILT_L2CAP))
return;
printf("Connect req: psm %d scid 0x%4.4x\n", psm, scid);
}
static inline void conn_rsp(int level, struct frame *frm)
{
l2cap_conn_rsp *h = frm->ptr;
uint16_t scid = btohs(h->scid);
uint16_t dcid = btohs(h->dcid);
uint16_t result = btohs(h->result);
uint16_t status = btohs(h->status);
uint16_t psm;
switch (h->result) {
case L2CAP_CR_SUCCESS:
if ((psm = get_psm(!frm->in, scid)))
add_cid(frm->in, frm->handle, dcid, psm);
break;
case L2CAP_CR_PEND:
break;
default:
del_cid(frm->in, dcid, scid);
break;
}
if (p_filter(FILT_L2CAP))
return;
printf("Connect rsp: dcid 0x%4.4x scid 0x%4.4x result %d status %d\n",
dcid, scid, result, status);
p_indent(level + 1, frm);
printf("%s", connresult2str(result));
if (result == 0x0001)
printf(" - %s\n", status2str(status));
else
printf("\n");
}
static void conf_rfc(void *ptr, int len, int in, uint16_t cid)
{
uint8_t mode;
mode = *((uint8_t *) ptr);
set_mode(in, cid, mode);
printf("RFC 0x%02x (%s", mode, mode2str(mode));
if (mode >= 0x01 && mode <= 0x04) {
uint8_t txwin, maxtrans;
uint16_t rto, mto, mps;
txwin = *((uint8_t *) (ptr + 1));
maxtrans = *((uint8_t *) (ptr + 2));
rto = btohs(bt_get_unaligned((uint16_t *) (ptr + 3)));
mto = btohs(bt_get_unaligned((uint16_t *) (ptr + 5)));
mps = btohs(bt_get_unaligned((uint16_t *) (ptr + 7)));
printf(", TxWin %d, MaxTx %d, RTo %d, MTo %d, MPS %d",
txwin, maxtrans, rto, mto, mps);
}
printf(")");
}
static void conf_fcs(void *ptr, int len)
{
uint8_t fcs;
fcs = *((uint8_t *) ptr);
printf("FCS Option");
if (len > 0)
printf(" 0x%2.2x (%s)", fcs, fcs2str(fcs));
}
static void conf_opt(int level, void *ptr, int len, int in, uint16_t cid)
{
p_indent(level, 0);
while (len > 0) {
l2cap_conf_opt *h = ptr;
ptr += L2CAP_CONF_OPT_SIZE + h->len;
len -= L2CAP_CONF_OPT_SIZE + h->len;
if (h->type & 0x80)
printf("[");
switch (h->type & 0x7f) {
case L2CAP_CONF_MTU:
set_mode(in, cid, 0x00);
printf("MTU");
if (h->len > 0)
printf(" %d", get_val(h->val, h->len));
break;
case L2CAP_CONF_FLUSH_TO:
printf("FlushTO");
if (h->len > 0)
printf(" %d", get_val(h->val, h->len));
break;
case L2CAP_CONF_QOS:
printf("QoS");
if (h->len > 0)
printf(" 0x%02x (%s)", *(h->val + 1), type2str(*(h->val + 1)));
break;
case L2CAP_CONF_RFC:
conf_rfc(h->val, h->len, in, cid);
break;
case L2CAP_CONF_FCS:
conf_fcs(h->val, h->len);
break;
default:
printf("Unknown (type %2.2x, len %d)", h->type & 0x7f, h->len);
break;
}
if (h->type & 0x80)
printf("] ");
else
printf(" ");
}
printf("\n");
}
static void conf_list(int level, uint8_t *list, int len)
{
int i;
p_indent(level, 0);
for (i = 0; i < len; i++) {
switch (list[i] & 0x7f) {
case L2CAP_CONF_MTU:
printf("MTU ");
break;
case L2CAP_CONF_FLUSH_TO:
printf("FlushTo ");
break;
case L2CAP_CONF_QOS:
printf("QoS ");
break;
case L2CAP_CONF_RFC:
printf("RFC ");
break;
case L2CAP_CONF_FCS:
printf("FCS ");
break;
default:
printf("%2.2x ", list[i] & 0x7f);
break;
}
}
printf("\n");
}
static inline void conf_req(int level, l2cap_cmd_hdr *cmd, struct frame *frm)
{
l2cap_conf_req *h = frm->ptr;
uint16_t dcid = btohs(h->dcid);
int clen = btohs(cmd->len) - L2CAP_CONF_REQ_SIZE;
if (p_filter(FILT_L2CAP))
return;
printf("Config req: dcid 0x%4.4x flags 0x%2.2x clen %d\n",
dcid, btohs(h->flags), clen);
if (clen > 0)
conf_opt(level + 1, h->data, clen, frm->in, dcid);
}
static inline void conf_rsp(int level, l2cap_cmd_hdr *cmd, struct frame *frm)
{
l2cap_conf_rsp *h = frm->ptr;
uint16_t scid = btohs(h->scid);
uint16_t result = btohs(h->result);
int clen = btohs(cmd->len) - L2CAP_CONF_RSP_SIZE;
if (p_filter(FILT_L2CAP))
return;
printf("Config rsp: scid 0x%4.4x flags 0x%2.2x result %d clen %d\n",
scid, btohs(h->flags), result, clen);
if (clen > 0) {
if (result) {
p_indent(level + 1, frm);
printf("%s\n", confresult2str(result));
}
if (result == 0x0003)
conf_list(level + 1, h->data, clen);
else
conf_opt(level + 1, h->data, clen, frm->in, scid);
} else {
p_indent(level + 1, frm);
printf("%s\n", confresult2str(result));
}
}
static inline void disconn_req(int level, struct frame *frm)
{
l2cap_disconn_req *h = frm->ptr;
if (p_filter(FILT_L2CAP))
return;
printf("Disconn req: dcid 0x%4.4x scid 0x%4.4x\n",
btohs(h->dcid), btohs(h->scid));
}
static inline void disconn_rsp(int level, struct frame *frm)
{
l2cap_disconn_rsp *h = frm->ptr;
uint16_t dcid = btohs(h->dcid);
uint16_t scid = btohs(h->scid);
del_cid(frm->in, dcid, scid);
if (p_filter(FILT_L2CAP))
return;
printf("Disconn rsp: dcid 0x%4.4x scid 0x%4.4x\n",
btohs(h->dcid), btohs(h->scid));
}
static inline void echo_req(int level, l2cap_cmd_hdr *cmd, struct frame *frm)
{
if (p_filter(FILT_L2CAP))
return;
printf("Echo req: dlen %d\n", btohs(cmd->len));
raw_dump(level, frm);
}
static inline void echo_rsp(int level, l2cap_cmd_hdr *cmd, struct frame *frm)
{
if (p_filter(FILT_L2CAP))
return;
printf("Echo rsp: dlen %d\n", btohs(cmd->len));
raw_dump(level, frm);
}
static void info_opt(int level, int type, void *ptr, int len)
{
uint32_t mask;
p_indent(level, 0);
switch (type) {
case 0x0001:
printf("Connectionless MTU %d\n", get_val(ptr, len));
break;
case 0x0002:
mask = get_val(ptr, len);
printf("Extended feature mask 0x%4.4x\n", mask);
if (parser.flags & DUMP_VERBOSE) {
if (mask & 0x01) {
p_indent(level + 1, 0);
printf("Flow control mode\n");
}
if (mask & 0x02) {
p_indent(level + 1, 0);
printf("Retransmission mode\n");
}
if (mask & 0x04) {
p_indent(level + 1, 0);
printf("Bi-directional QoS\n");
}
}
break;
case 0x0003:
printf("Fixed channel list\n");
break;
default:
printf("Unknown (len %d)\n", len);
break;
}
}
static inline void info_req(int level, l2cap_cmd_hdr *cmd, struct frame *frm)
{
l2cap_info_req *h = frm->ptr;
if (p_filter(FILT_L2CAP))
return;
printf("Info req: type %d\n", btohs(h->type));
}
static inline void info_rsp(int level, l2cap_cmd_hdr *cmd, struct frame *frm)
{
l2cap_info_rsp *h = frm->ptr;
uint16_t type = btohs(h->type);
uint16_t result = btohs(h->result);
int ilen = btohs(cmd->len) - L2CAP_INFO_RSP_SIZE;
if (p_filter(FILT_L2CAP))
return;
printf("Info rsp: type %d result %d\n", type, result);
if (ilen > 0) {
info_opt(level + 1, type, h->data, ilen);
} else {
p_indent(level + 1, frm);
printf("%s\n", inforesult2str(result));
}
}
static void l2cap_parse(int level, struct frame *frm)
{
l2cap_hdr *hdr = (void *)frm->ptr;
uint16_t dlen = btohs(hdr->len);
uint16_t cid = btohs(hdr->cid);
uint16_t psm;
frm->ptr += L2CAP_HDR_SIZE;
frm->len -= L2CAP_HDR_SIZE;
if (cid == 0x1) {
/* Signaling channel */
while (frm->len >= L2CAP_CMD_HDR_SIZE) {
l2cap_cmd_hdr *hdr = frm->ptr;
frm->ptr += L2CAP_CMD_HDR_SIZE;
frm->len -= L2CAP_CMD_HDR_SIZE;
if (!p_filter(FILT_L2CAP)) {
p_indent(level, frm);
printf("L2CAP(s): ");
}
switch (hdr->code) {
case L2CAP_COMMAND_REJ:
command_rej(level, frm);
break;
case L2CAP_CONN_REQ:
conn_req(level, frm);
break;
case L2CAP_CONN_RSP:
conn_rsp(level, frm);
break;
case L2CAP_CONF_REQ:
conf_req(level, hdr, frm);
break;
case L2CAP_CONF_RSP:
conf_rsp(level, hdr, frm);
break;
case L2CAP_DISCONN_REQ:
disconn_req(level, frm);
break;
case L2CAP_DISCONN_RSP:
disconn_rsp(level, frm);
break;
case L2CAP_ECHO_REQ:
echo_req(level, hdr, frm);
break;
case L2CAP_ECHO_RSP:
echo_rsp(level, hdr, frm);
break;
case L2CAP_INFO_REQ:
info_req(level, hdr, frm);
break;
case L2CAP_INFO_RSP:
info_rsp(level, hdr, frm);
break;
default:
if (p_filter(FILT_L2CAP))
break;
printf("code 0x%2.2x ident %d len %d\n",
hdr->code, hdr->ident, btohs(hdr->len));
raw_dump(level, frm);
}
if (frm->len > btohs(hdr->len)) {
frm->len -= btohs(hdr->len);
frm->ptr += btohs(hdr->len);
} else
frm->len = 0;
}
} else if (cid == 0x2) {
/* Connectionless channel */
if (p_filter(FILT_L2CAP))
return;
psm = btohs(bt_get_unaligned((uint16_t *) frm->ptr));
frm->ptr += 2;
frm->len -= 2;
p_indent(level, frm);
printf("L2CAP(c): len %d psm %d\n", dlen, psm);
raw_dump(level, frm);
} else {
/* Connection oriented channel */
uint8_t mode = get_mode(!frm->in, cid);
uint16_t psm = get_psm(!frm->in, cid);
uint16_t ctrl = 0, fcs = 0;
uint32_t proto;
frm->cid = cid;
frm->num = get_num(!frm->in, cid);
if (mode > 0) {
ctrl = btohs(bt_get_unaligned((uint16_t *) frm->ptr));
frm->ptr += 2;
frm->len -= 4;
fcs = btohs(bt_get_unaligned((uint16_t *) (frm->ptr + frm->len)));
}
if (!p_filter(FILT_L2CAP)) {
p_indent(level, frm);
printf("L2CAP(d): cid 0x%4.4x len %d", cid, dlen);
if (mode > 0)
printf(" ctrl 0x%4.4x fcs 0x%4.4x", ctrl, fcs);
printf(" [psm %d]\n", psm);
level++;
if (mode > 0) {
p_indent(level, frm);
printf("%s:", ctrl & 0x01 ? "S-frame" : "I-frame");
if (ctrl & 0x01) {
printf(" %s", supervisory2str((ctrl & 0x0c) >> 2));
} else {
uint8_t sar = (ctrl & 0xc000) >> 14;
printf(" %s", sar2str(sar));
if (sar == 1) {
uint16_t len;
len = btohs(bt_get_unaligned((uint16_t *) frm->ptr));
frm->ptr += 2;
frm->len -= 2;
printf(" (len %d)", len);
}
printf(" TxSeq %d", (ctrl & 0x7e) >> 1);
}
printf(" ReqSeq %d", (ctrl & 0x3f00) >> 8);
if (ctrl & 0x80)
printf(" F-bit");
if (ctrl & 0x10)
printf(" P-bit");
printf("\n");
}
}
switch (psm) {
case 0x01:
if (!p_filter(FILT_SDP))
sdp_dump(level + 1, frm);
else
raw_dump(level + 1, frm);
break;
case 0x03:
if (!p_filter(FILT_RFCOMM))
rfcomm_dump(level, frm);
else
raw_dump(level + 1, frm);
break;
case 0x0f:
if (!p_filter(FILT_BNEP))
bnep_dump(level, frm);
else
raw_dump(level + 1, frm);
break;
case 0x11:
case 0x13:
if (!p_filter(FILT_HIDP))
hidp_dump(level, frm);
else
raw_dump(level + 1, frm);
break;
case 0x17:
if (!p_filter(FILT_AVCTP))
avctp_dump(level, frm);
else
raw_dump(level + 1, frm);
break;
case 0x19:
if (!p_filter(FILT_AVDTP))
avdtp_dump(level, frm);
else
raw_dump(level + 1, frm);
break;
case 0x1f:
if (!p_filter(FILT_ATT))
att_dump(level, frm);
else
raw_dump(level + 1, frm);
break;
default:
proto = get_proto(frm->handle, psm, 0);
switch (proto) {
case SDP_UUID_CMTP:
if (!p_filter(FILT_CMTP))
cmtp_dump(level, frm);
else
raw_dump(level + 1, frm);
break;
case SDP_UUID_HARDCOPY_CONTROL_CHANNEL:
if (!p_filter(FILT_HCRP))
hcrp_dump(level, frm);
else
raw_dump(level + 1, frm);
break;
default:
if (p_filter(FILT_L2CAP))
break;
raw_dump(level, frm);
break;
}
break;
}
}
}
void l2cap_dump(int level, struct frame *frm)
{
struct frame *fr;
l2cap_hdr *hdr;
uint16_t dlen;
if ((frm->flags & ACL_START) || frm->flags == ACL_START_NO_FLUSH) {
hdr = frm->ptr;
dlen = btohs(hdr->len);
if ((int) frm->len == (dlen + L2CAP_HDR_SIZE)) {
/* Complete frame */
l2cap_parse(level, frm);
return;
}
if (!(fr = get_frame(frm->handle))) {
fprintf(stderr, "Not enough connection handles\n");
raw_dump(level, frm);
return;
}
if (fr->data)
free(fr->data);
if (!(fr->data = malloc(dlen + L2CAP_HDR_SIZE))) {
perror("Can't allocate L2CAP reassembly buffer");
return;
}
memcpy(fr->data, frm->ptr, frm->len);
fr->data_len = dlen + L2CAP_HDR_SIZE;
fr->len = frm->len;
fr->ptr = fr->data;
fr->dev_id = frm->dev_id;
fr->in = frm->in;
fr->ts = frm->ts;
fr->handle = frm->handle;
fr->cid = frm->cid;
fr->num = frm->num;
fr->dlci = frm->dlci;
fr->channel = frm->channel;
fr->pppdump_fd = frm->pppdump_fd;
fr->audio_fd = frm->audio_fd;
} else {
if (!(fr = get_frame(frm->handle))) {
fprintf(stderr, "Not enough connection handles\n");
raw_dump(level, frm);
return;
}
if (!fr->data) {
/* Unexpected fragment */
raw_dump(level, frm);
return;
}
if (frm->len > (fr->data_len - fr->len)) {
/* Bad fragment */
raw_dump(level, frm);
free(fr->data); fr->data = NULL;
return;
}
memcpy(fr->data + fr->len, frm->ptr, frm->len);
fr->len += frm->len;
if (fr->len == fr->data_len) {
/* Complete frame */
l2cap_parse(level, fr);
free(fr->data); fr->data = NULL;
return;
}
}
}
void l2cap_clear(uint16_t handle)
{
del_handle(handle);
}