L2CAP layer in bluetooth development – bluez programming in c

L2CAP layer in bluetooth development – bluez programming in c
Core and Adopted Protocols · L2CAP Channels · CID Table · BlueZ Code
🧩
L2CAP
🔁
RFCOMM
🔍
SDP
📋
Profiles

What This Post Covers

Hello students welcome to embeddedpathashalas free bluetooth development course in c, using bluez. The previous chapter covered the lower layers — Radio, Baseband, LMP, and HCI. This post goes one level up and explains the upper layers of the Bluetooth Classic protocol stack. These layers sit above HCI and do the heavier lifting: multiplexing data streams, emulating serial ports, discovering services, and enabling audio streaming.

This is part one of the upper layers series. It covers the protocol stack architecture, the difference between core and adopted protocols, profiles, and the most important upper layer — L2CAP in detail. More layers (RFCOMM, SDP, OBEX) will be covered in the next post.

💡 Prerequisite: You should be familiar with HCI commands and the Baseband layer. Check the Bluetooth Lower Layers post if needed.

Key Terms in This Post

L2CAP layer in bluetooth bluetooth development bluez programming in c L2CAP CID Channel RFCOMM SDP OBEX AVDTP AVCTP Core Protocol Adopted Protocol Profile ACL Link QoS Connection-Oriented Connectionless BlueZ

1. The Upper Layers — Quick Overview
Core Protocols Adopted Protocols Profiles Stack Architecture

The upper layers of the Bluetooth Classic stack sit above the HCI interface. They provide richer services — like multiplexing multiple data streams, emulating a serial cable, exchanging files, or streaming audio — that applications can directly use.

The protocols in this region fall into two groups:

Core Protocols vs Adopted Protocols

🔵 Core Protocols 🟢 Adopted Protocols

Written from scratch specifically for Bluetooth

L2CAP — Data multiplexing SDP — Service Discovery AVDTP — Audio/Video streaming AVCTP — AV remote control

Taken from other standards and reused

RFCOMM — From ETSI TS 07.10 (serial port) OBEX — From IrOBEX (file exchange)
💡 Why adopt existing protocols? Reusing RFCOMM meant legacy serial port applications could run over Bluetooth with zero changes. Reusing OBEX meant apps that already worked over Infrared (IrDA) could instantly run over Bluetooth.

Figure 1 — Bluetooth Classic Stack: Upper Layers Position

GOEP SDAP HFP FTP SPP A2DP AVRCP
← Profiles (dotted border)
OBEX (Adopted)
SDP RFCOMM (Adopted) AVDTP AVCTP
L2CAP  (Core Protocol)
Host Controller Interface (HCI)
Link Manager Protocol (LMP)
Baseband Controller
Bluetooth Radio
Core Protocol Adopted ProtocolProfile

A Profile is not a protocol — it is a specification that describes which protocols to use and which features of each protocol to enable in order to support a particular use case. For example, the File Transfer Profile (FTP) tells you to use OBEX on top of RFCOMM on top of L2CAP, with specific options set. Profiles make sure two devices from different manufacturers work together for the same task.

2. L2CAP — What It Does and Why It Exists
Data Multiplexing 64 KB Packets ACL Links Segmentation

L2CAP stands for Logical Link Control and Adaptation Protocol. It sits directly above the Baseband layer (accessed through HCI) and is the first upper-layer protocol that every other upper layer passes through.

Think of L2CAP as a smart pipe manager. The Bluetooth radio gives you one physical ACL link between two devices. L2CAP lets multiple protocols share that single link at the same time — each gets its own logical channel. It also handles breaking large data packets into smaller Baseband-sized chunks (segmentation) and reassembling them at the other end.

What L2CAP Provides to Upper Layers

📦 Protocol Multiplexing
Multiple protocols (RFCOMM, SDP, AVDTP) share one ACL link through separate channels
✂️ Segmentation & Reassembly
Upper layers can send up to 64 KB in one shot. L2CAP splits it to fit Baseband packets and reassembles at the other end
📶 QoS Negotiation
Connection-oriented channels can negotiate Quality of Service parameters like bandwidth and latency
📡 Connectionless Broadcast
The Master can broadcast data to all Slaves in the piconet without setting up individual connections

3. L2CAP Channels — Connection-Oriented vs Connectionless
Connection-Oriented Connectionless L2CAP_CONNECT_REQ L2CAP_DISCONNECT_REQ

A channel in L2CAP is a logical data path between two L2CAP entities on different devices. There are two types:

L2CAP Channel Types

Connection-Oriented Channel Connectionless Channel
  • Point-to-point data between two specific devices
  • Must be set up first with L2CAP_CONNECTION_REQUEST
  • Can have QoS (bandwidth, latency) negotiated on setup
  • Must be closed with L2CAP_DISCONNECT_REQUEST when done
  • Used by: RFCOMM, AVDTP, AVCTP
  • No setup or teardown procedure needed
  • Used by Master to broadcast to all Slaves in a piconet
  • Can also carry unicast data (one device to another)
  • Lower overhead — no connection latency
  • No acknowledgement or retransmission

Connection-Oriented Channel Flow

Figure 2 — L2CAP Connection-Oriented Channel Lifecycle

Device A (Initiator) Device B (Responder)

L2CAP_CONNECTION_REQUEST

L2CAP_CONNECTION_RESPONSE

L2CAP_CONFIGURATION_REQUEST (QoS, MTU)

L2CAP_CONFIGURATION_RESPONSE

✅ Channel open — data transfer begins (up to 64 KB per packet)

L2CAP_DISCONNECT_REQUEST

L2CAP_DISCONNECT_RESPONSE

🔴 Channel closed
Request ResponseDisconnect

4. Channel Identifiers (CIDs)
CID Fixed Channels Dynamic Channels Signaling Channel

Every endpoint of an L2CAP channel is identified by a Channel Identifier (CID). A CID is a local number — it is assigned by the L2CAP layer on each device independently. So Device A might assign CID 0x0040 for a channel while Device B assigns CID 0x0041 for the same channel on its side. Both are valid.

CIDs in the range 0x0001–0x003F are reserved as fixed channels — they are always available once an ACL link exists and do not need to be set up separately. CIDs from 0x0040 onwards are dynamically assigned by L2CAP for new connection-oriented channels.

Table 1 — L2CAP CID Name Space

CID Description
0x0000 Null identifier — not allowed in any packet
0x0001 L2CAP Signaling Channel (BR/EDR) — used to send all signaling commands: Connection Req/Rsp · Configuration Req/Rsp · Disconnection Req/Rsp · Echo Req/Rsp
0x0002 Connectionless channel — two uses:
① Master broadcasts to all Slaves in the piconet (no ACK, no retransmit)
② Unicast from Master or Slave to a single device
0x0003 AMP Manager Protocol — used for Bluetooth 3.0 + HS operations (alternate MAC/PHY)
0x0004 Attribute Protocol (ATT) — used by BLE GATT for reading/writing attributes (covered in later posts)
0x0005 LE L2CAP Signaling Channel — same role as 0x0001 but for BLE connections
0x0006 Security Manager Protocol (SMP) — handles BLE pairing and key distribution (covered in later posts)
0x0007–0x003E Reserved for future use
0x0040–0xFFFF Dynamically allocated — assigned by L2CAP when a new connection-oriented channel is opened. Each device assigns its own CID independently.
📌 Key point: Fixed channels (0x0001–0x003F) are available immediately after an ACL link is established. No connection setup is needed to use them. Dynamic channels (0x0040+) must be explicitly opened using the signaling channel first.

5. L2CAP in BlueZ — Practical Code
l2cap_socket SOCK_SEQPACKET sockaddr_l2 PSM

In BlueZ, L2CAP is exposed through standard Linux sockets with AF_BLUETOOTH address family and BTPROTO_L2CAP protocol. You work with it just like TCP sockets — create, bind, connect, send, receive, close.

One important concept: when opening a dynamic L2CAP channel you specify a PSM (Protocol/Service Multiplexer) rather than a port number. The PSM identifies which upper-layer protocol the channel is for (e.g., RFCOMM uses PSM 3, AVDTP uses PSM 25).

L2CAP Server — Listen for Incoming Channel

/*
 * l2cap_server.c — Accept an incoming L2CAP connection
 * Compile: gcc l2cap_server.c -o l2cap_server -lbluetooth
 * Run:     sudo ./l2cap_server
 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>

#define L2CAP_PSM   0x1001  /* custom PSM — must be odd, >= 0x1001 */
#define BUFSIZE     1024

int main() {
    struct sockaddr_l2 addr = {0};
    char buf[BUFSIZE];
    int server_sock, client_sock;
    socklen_t opt = sizeof(addr);

    /* 1. Create an L2CAP socket */
    server_sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);

    /* 2. Bind to local adapter (BDADDR_ANY = first available) on our PSM */
    addr.l2_family  = AF_BLUETOOTH;
    addr.l2_psm     = htobs(L2CAP_PSM);
    bacpy(&addr.l2_bdaddr, BDADDR_ANY);
    bind(server_sock, (struct sockaddr *)&addr, sizeof(addr));

    /* 3. Listen for incoming connections */
    listen(server_sock, 1);
    printf("Waiting for L2CAP connection on PSM 0x%04x ...\n", L2CAP_PSM);

    /* 4. Accept the connection — this blocks until a client connects */
    client_sock = accept(server_sock, (struct sockaddr *)&addr, &opt);

    char remote[18];
    ba2str(&addr.l2_bdaddr, remote);
    printf("Connected from: %s\n", remote);

    /* 5. Read data from the channel */
    int bytes = read(client_sock, buf, BUFSIZE);
    if (bytes > 0) {
        buf[bytes] = '\0';
        printf("Received: %s\n", buf);
    }

    close(client_sock);
    close(server_sock);
    return 0;
}

L2CAP Client — Connect and Send Data

/*
 * l2cap_client.c — Connect to an L2CAP server and send data
 * Compile: gcc l2cap_client.c -o l2cap_client -lbluetooth
 * Run:     sudo ./l2cap_client
 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/l2cap.h>

#define REMOTE_BDADDR  "00:1A:7D:DA:71:11"  /* replace with your device */
#define L2CAP_PSM      0x1001

int main() {
    struct sockaddr_l2 addr = {0};
    int sock;

    /* 1. Create the L2CAP socket */
    sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);

    /* 2. Fill in the remote device address and PSM */
    addr.l2_family = AF_BLUETOOTH;
    addr.l2_psm    = htobs(L2CAP_PSM);
    str2ba(REMOTE_BDADDR, &addr.l2_bdaddr);

    /* 3. Connect — this sends L2CAP_CONNECTION_REQUEST under the hood */
    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("connect failed");
        return 1;
    }
    printf("Connected to %s\n", REMOTE_BDADDR);

    /* 4. Send a message */
    const char *msg = "Hello from L2CAP client";
    write(sock, msg, strlen(msg));
    printf("Sent: %s\n", msg);

    close(sock);
    return 0;
}

Run It — Step by Step

# Terminal 1 — On the server machine (the device receiving the connection)
sudo hciconfig hci0 pscan        # make it connectable
gcc l2cap_server.c -o l2cap_server -lbluetooth
sudo ./l2cap_server

# Terminal 2 — On the client machine (the device initiating)
gcc l2cap_client.c -o l2cap_client -lbluetooth
sudo ./l2cap_client

# Expected output on server:
# Waiting for L2CAP connection on PSM 0x1001 ...
# Connected from: 00:1A:7D:DA:71:11
# Received: Hello from L2CAP client

Check L2CAP Channel from Terminal

# List all open L2CAP connections on your adapter
sudo btmgmt info

# Or use l2ping to test L2CAP connectivity to a remote device
sudo l2ping 00:1A:7D:DA:71:11

# Output:
# Ping: 00:1A:7D:DA:71:11 from 00:11:22:33:44:55 (data size 44) ...
# 0 bytes from 00:1A:7D:DA:71:11 id 200 time 12.34ms
# 0 bytes from 00:1A:7D:DA:71:11 id 201 time 11.78ms
⚠️ PSM rules: Custom PSMs for your own application must be odd numbers and must be ≥ 0x1001. PSMs below 0x1001 are reserved for standard profiles (RFCOMM = 0x0003, AVDTP = 0x0019, AVCTP = 0x0017, etc.).

6. Quick Reference — Upper Layer Protocols
Protocol Type Sits On What It Does
L2CAP Core HCI / Baseband Data multiplexing, segmentation, QoS
SDP Core L2CAP Discover what services a remote device offers
RFCOMM Adopted L2CAP Serial port emulation — up to 60 virtual COM ports
OBEX Adopted RFCOMM Object/file exchange (files, contacts, calendar)
AVDTP Core L2CAP Audio/video streaming transport (used by A2DP)
AVCTP Core L2CAP AV remote control commands (used by AVRCP)

✏️ RFCOMM, SDP, OBEX and profiles (A2DP, HFP, SPP, FTP) will each be covered in dedicated posts in this series.

Next Up: RFCOMM & SDP

The next post covers RFCOMM serial port emulation and SDP service discovery — with BlueZ examples for both. Part of the free Bluetooth course on EmbeddedPathashala.

Go to Courses BLE Deep Dive →

Leave a Reply

Your email address will not be published. Required fields are marked *