L2CAP implementation in Android
Part 1: https://medium.com/@girishby90/android-ble-l2cap-tutorial-3b8ff0994ec8
L2CAP implementation in Android

Part 1: https://medium.com/@girishby90/android-ble-l2cap-tutorial-3b8ff0994ec8
If you do not read part one, I suggest you read it once. It gives basics idea about the BLE concept in android
What is L2CAP?
Logical Link Control and Adaptation Protocol (L2CAP) is a protocol used in the Bluetooth standard that provides adaption between higher layers and the baseband layer of the Bluetooth stack.
The Logical Link Control and Adaptation Protocol (L2CAP) is a layer in the Bluetooth stack that provides crucial services for communication between Bluetooth devices. L2CAP offers segmentation and reassembly services for large packets to be transmitted across Bluetooth links and also allows for the multiplexing of higher-layer protocols and services.
Functions of L2CAP Layer
-
It accepts packets of up to 64 kB from upper layers and breaks them down into smaller frames for transmission. These frames are then reassembled into packets again at the receiving end.
-
L2CAP manages the multiplexing and demultiplexing of multiple packet sources. Once a packet has been reassembled, the L2CAP layer determines which upper-layer protocol it should be passed to, such as RF communication or telephony.
-
L2CAP also handles quality of service requirements, both during link establishment and normal operation. This includes negotiating maximum payload size to prevent large-packet devices from overwhelming small-packet devices. This is important because not all devices can handle the maximum packet size of 64 kB. The L2CAP layer corresponds with the 802 Data Link Layer which is typically responsible for transmission, framing, and error control over a particular link. As such, L2CAP overlaps the link controller task and the control end of the baseband, including error checking and correction.
In the L2CAP protocol, there are three specific fields that are used to transfer data:
The Length field is a 16-bit field that defines the size of the data, in bytes, coming from the upper layers.
The Channel ID (CID) field is a 16-bit field that serves as a unique identifier for the virtual channel created at this level.
The Data field is where the actual payload is placed. The maximum size of the data in this field is 65535 bytes.
Let’s look into the code part.
3 main functions will be used to transfer the data between the two devices.
1. available()
2. read()
3. write(byte b[])
We can use the above function part of the InputStream and OutputStream class
Step 1: Initialize the Bluetooth socket
private lateinit var l2capSocket: BluetoothSocket
l2capSocket = device.createInsecureL2capChannel(int psm) // pass an intereger PSM value
l2capSocket.connect()
The interface for Bluetooth Sockets is similar to that of TCP sockets: [Socket](https://developer.android.com/reference/java/net/Socket) and [ServerSocket](https://developer.android.com/reference/java/net/ServerSocket). On the server side, use a [BluetoothServerSocket](https://developer.android.com/reference/android/bluetooth/BluetoothServerSocket) to create a listening server socket. When a connection is accepted by the [BluetoothServerSocket](https://developer.android.com/reference/android/bluetooth/BluetoothServerSocket), it will return a new [BluetoothSocket](https://developer.android.com/reference/android/bluetooth/BluetoothSocket) to manage the connection. On the client side, use a single [BluetoothSocket](https://developer.android.com/reference/android/bluetooth/BluetoothSocket) to both initiate an outgoing connection and to manage the connection.
Connect() will make an attempt to connect to a remote device.
Step 2: Check Available bytes from the InputStream
val avail = l2capSocket.inputStream.available()
Returns an estimate of the number of bytes that can be read (or skipped over) from this input stream without blocking, which may be 0, or 0 when end of stream is detected. The read might be on the same thread or another thread. A single read or skip of this many bytes will not block, but may read or skip fewer bytes.
Step 3: Read from the input stream
val tempBuf = ByteArray(avail)
l2capSocket.inputStream.read(tempBuf)
avail is the available bytes(step 2), we are using it to create a temp byte array.
Reads some number of bytes from the input stream and stores them into the buffer array tempBuf. The number of bytes actually read is returned as an integer. This method blocks until input data is available, end of file is detected, or an exception is thrown.
If the length of tempBuf is zero, then no bytes are read and 0 is returned; otherwise, there is an attempt to read at least one byte. If no byte is available because the stream is at the end of the file, the value -1 is returned; otherwise, at least one byte is read and stored into tempBuf. The first byte read is stored into element tempBuf[0], the next one into tempBuf[1], and so on. The number of bytes read is, at most, equal to the length of tempBuf.
Step 3: Write to the output stream
l2capSocket.outputStream.write(bytes) // pass bytess array which you want to write
Writes b.length bytes from the specified byte array to this output stream.
It’s always a best practice to close the socket connection once your work is done
Step 4: Closes this stream and releases any system resources associated with it.
l2capSocket.close() 메타데이터
- post_id
- 588f5b867f01
- slug
- l2cap-implementation-in-android-588f5b867f01
- url
- https://medium.com/@girishby90/l2cap-implementation-in-android-588f5b867f01
- canonical_url
- https://medium.com/@girishby90/l2cap-implementation-in-android-588f5b867f01
- author_url
- https://medium.com/@girishby90
- status
- ok
- fetched_at
- 2026-06-29 01:02:39