← Back to list

Android-BLE L2Cap Tutorial

Hello everyone…!!

Girish Yadawad · 2023-03-24 04:58 · 2 claps · 3.6 min read
#android #ble #l2cap #bluetooth #gatt
Open on Medium ↗

Android-BLE L2Cap Tutorial

Hello everyone…!!

Welcome to the Bluetooth L2cap tutorial, in this blog, we will learn how we can make connections with central using l2cap, How to read and write from peripheral and central, and vice-versa.

For beginners, I strongly recommend having a basic understanding of BLE before reading this article. **punchthrough** blog helped me to get better understanding of BLE because this was my first assignment on Bluetooth.

I was assigned the task of making an android app for a central which will exchange large payloads with the help of l2cap.

After doing a lot of research on the internet, I found that there is not much content available to implement l2cap. Writing this blog to share my knowledge with others.

Definition of some of the keywords that are used in this article.

Central/Client A device that scans for and connects to BLE peripherals in order to perform some operation. In the context of app development, this is typically an Android device.

Peripheral/Server A device that advertises its presence and is connected to by a central in order to accomplish some task. In the context of app development, this is typically a BLE device you’re working with, like a heart rate monitor.

Alright, enough of the story, let’s jump into the action

Photo by Aziz Acharki on Unsplash

Photo by Aziz Acharki on Unsplash

In our Android app, we have to begin the search operation for ble devices, for that we create a simple layout with a button and name it as “search BLE”. On click of that button, we need to perform the search for central/server(central can be a firmware and it should be advertising for the discovery).

Add the below permission in your manifest file.

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

If you’re targeting to the latest SDK, you need to add some extra permissions as below.

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

Make sure you are handling the runtime permissions for location on Bluetooth. otherwise, you will get a crash when you run the application.

Step 1: Define a Bluetooth adapter

Represents the local device Bluetooth adapter. The [BluetoothAdapter](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter) lets you perform fundamental Bluetooth tasks, such as initiate device discovery, query a list of bonded (paired) devices, instantiate a [BluetoothDevice](https://developer.android.com/reference/android/bluetooth/BluetoothDevice) using a known MAC address, and create a [BluetoothServerSocket](https://developer.android.com/reference/android/bluetooth/BluetoothServerSocket) to listen for connection requests from other devices, and start a scan for Bluetooth LE devices.

private val bluetoothAdapter: BluetoothAdapter by lazy {
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    bluetoothManager.adapter
}

Step 2: Define a BleScanner

This class provides methods to perform scan related operations for Bluetooth LE devices. An application can scan for a particular type of Bluetooth LE devices using [ScanFilter](https://developer.android.com/reference/android/bluetooth/le/ScanFilter). It can also request different types of callbacks for delivering the result.

private val bleScanner by lazy {
    bluetoothAdapter.bluetoothLeScanner
}

Step 3: Define a scan filter(optional)

Criteria for filtering result from Bluetooth LE scans. A ScanFilter allows clients to restrict scan results to only those that are of interest to them. This is optional.

val filters: MutableList<ScanFilter> = ArrayList()

val macAddressFilter = ScanFilter.Builder()
                .setDeviceAddress("xx:xx:xx:xx:xx:xx") // add your device mac address here
                .build()

filters.add(macAddressFilter)

Step 4: We can add scan settings for the Bluetooth scan as below

private val scanSettings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
    .build()

Different types of scan modes are,

The scan mode can be one of SCAN_MODE_LOW_POWER, SCAN_MODE_BALANCED or SCAN_MODE_LOW_LATENCY.

Read more about the Scan setting in the official document

Step 5: Important step, once we start the scan, we need a callback function to get the result, so let’s define a callback fun.

private val scanCallback = object : ScanCallback() {
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        if (result.isConnectable){
            // result.device will consist of central details
            Log.i("ScanCallBack","Device Name: ${result.device.name?:"No Name"}")
            Log.i("ScanCallBack","Device Address: ${result.device.address}")
        }
    }
    override fun onScanFailed(errorCode: Int) {
        Timber.e("onScanFailed: code $errorCode")
    }
}

Step 6: Final step for discovering the central, In this step, you will start scanning and we will receive the results in the callback function created in step 5.

If you don’t have a scan filter, then you can pass the value as null in the first param

bleScanner.startScan(null, scanSettings, scanCallback)

Or else use the below line of code

bleScanner.startScan(filters, scanSettings, scanCallback)

That’s all, When you Run the app and click on the start scan button you will see the result in onScanResult call back function.

So the final scan function will look like this.

private fun startBleScan() {
        val filters: MutableList<ScanFilter> = ArrayList()

        val macAddressFilter = ScanFilter.Builder()
            .setDeviceAddress("xx:xx:xx:xx:xx:xx")
            .build()
        filters.add(macAddressFilter)

        /*val macAddressDevBoard = ScanFilter.Builder()
            .setDeviceAddress("84:71:27:44:36:02")
            .build()
        filters.add(macAddressDevBoard)*/

        scanResultAdapter.notifyDataSetChanged()
        bleScanner.startScan(null, scanSettings, scanCallback)
        isScanning = true

        mHandler.postDelayed({
            stopBleScan()
            progress_horizontal.visibility = View.GONE }, 20000)
}

If you see a crash after running this code, make sure to add runtime permission of Bluetooth and location

So the next step will learn about l2cap, and how we can use it for payload exchange.

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.

**Part 2: L2cap implementation**


메타데이터
post_id
3b8ff0994ec8
slug
android-ble-l2cap-tutorial-3b8ff0994ec8
url
https://medium.com/@girishby90/android-ble-l2cap-tutorial-3b8ff0994ec8
canonical_url
https://medium.com/@girishby90/android-ble-l2cap-tutorial-3b8ff0994ec8
author_url
https://medium.com/@girishby90
status
ok
fetched_at
2026-06-29 01:02:39