Home » Android » android – How to transfer images via Bluetooth (LE) to a desktop application

android – How to transfer images via Bluetooth (LE) to a desktop application

Posted by: admin June 15, 2020 Leave a comment


We are currently trying to implement the transmission of images from a mobile device (in this case an IPhone) to a desktop application. We tried already the Bluetooth Serial plugin which works fine for Android but does not list any devices when scanning for our desktop application.

To cover iOS support (AFAIK iOS only supports BluetoothLE), we reimplemented our desktop application to use BluetoothLE and behave like a peripheral. Also we altered our Ionic application to use BLE plugin.

Now BluetoothLE only supports the transmission of packages with the size of 20 Byte whilst our image is about 500kb big. So we could obviously split our image into chunks and transmit it with the following function (taken from this gist):

function writeLargeData(buffer) {
    console.log('writeLargeData', buffer.byteLength, 'bytes in',MAX_DATA_SEND_SIZE, 'byte chunks.');
    var chunkCount = Math.ceil(buffer.byteLength / MAX_DATA_SEND_SIZE);
    var chunkTotal = chunkCount;
    var index = 0;
    var startTime = new Date();

    var transferComplete = function () {
        console.log("Transfer Complete");

    var sendChunk = function () {
        if (!chunkCount) {
            return; // so we don't send an empty buffer

        console.log('Sending data chunk', chunkCount + '.');

        var chunk = buffer.slice(index, index + MAX_DATA_SEND_SIZE);
        index += MAX_DATA_SEND_SIZE;

            sendChunk,         // success callback - call sendChunk() (recursive)
            function(reason) { // error callback
                console.log('Write failed ' + reason);
    // send the first chunk

Still this would mean for us that we would have to launch about 25k transmissions which I assume will take a long time to complete. Now I wonder why is that the data transmission via Bluetooth is that handicapped.

How to&Answers:

If you want to try out L2CAP your could modify your Central desktop app somehow like this:

private let characteristicUUID = CBUUID(string: CBUUIDL2CAPPSMCharacteristicString)

Then advertize and publish a L2CAP channel:

let service = CBMutableService(type: peripheralUUID, primary: true)

let properties: CBCharacteristicProperties = [.read, .indicate]
let permissions: CBAttributePermissions = [.readable]

let characteristic = CBMutableCharacteristic(type: characteristicUUID, properties: properties, value: nil, permissions: permissions)
self.characteristic = characteristic
service.characteristics = [characteristic]

self.manager.publishL2CAPChannel(withEncryption: false)
let data = [CBAdvertisementDataLocalNameKey : "Peripherial-42", CBAdvertisementDataServiceUUIDsKey: [peripheralUUID]] as [String : Any]

In your

func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {

respective your

func peripheralManager(_ peripheral: CBPeripheralManager, didPublishL2CAPChannel PSM: CBL2CAPPSM, error: Error?) {

offer the PSM value (= kind of socket handle (UInt16), for Bluetooth stream connections):

let data = withUnsafeBytes(of: PSM) { Data($0) }
if let characteristic = self.characteristic {
    characteristic.value = data
    self.manager.updateValue(data, for: characteristic, onSubscribedCentrals: self.subscribedCentrals)

finally in

func peripheralManager(_ peripheral: CBPeripheralManager, didOpen channel: CBL2CAPChannel?, error: Error?) 

open an input stream:

channel.inputStream.delegate = self
channel.inputStream.schedule(in: RunLoop.current, forMode: .default)

where the delegate could look something like this:

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case Stream.Event.hasBytesAvailable:
        if let stream = aStream as? InputStream {
            //buffer is some UnsafeMutablePointer<UInt8>
            let read = stream.read(buffer, maxLength: capacity)
            print("\(read) bytes read")
    case ...

iOS app with Central Role

Assuming you have something like that in your iOS code:

func sendImage(imageData: Data) {
    self.manager = CBCentralManager(delegate: self, queue: nil)
    self.imageData = imageData
    self.bytesToWrite = imageData.count

then you can modify your peripheral on your iOS client to work with the L2Cap channel like this:

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    if let characteristicValue = characteristic.value {
        let psm = characteristicValue.withUnsafeBytes {
            $0.load(as: UInt16.self)
        print("using psm \(psm) for l2cap channel!")

and as soon as you are notified of the opened channel, open the output stream on it:

func peripheral(_ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) 
    channel.outputStream.delegate = self.streamDelegate
    channel.outputStream.schedule(in: RunLoop.current, forMode: .default)

Your supplied stream delegate might look like this:

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case Stream.Event.hasSpaceAvailable:
        if let stream = aStream as? OutputStream, let imageData = self.imageData {
            if self.bytesToWrite > 0 {
                let bytesWritten = imageData.withUnsafeBytes {
                        $0.advanced(by: totalBytes),
                        maxLength: self.bytesToWrite
                self.bytesToWrite -= bytesWritten
                self.totalBytes += bytesWritten
                print("\(bytesWritten) bytes written, \(bytesToWrite) remain")
            } else {
    case ...

There is a cool WWDC video from 2017, What’s New in Core Bluetooth, see here https://developer.apple.com/videos/play/wwdc2017/712/

At around 14:45 it starts to discuss how L2Cap channels are working.

At 28:47, the Get the Most out of Core Bluetooth topic starts, in which performance-related things are discussed in detail. That’s probably exactly what you’re interested in.

Finally, at 37:59 you will see various possible throughputs in kbps.
Based on the data shown on the slide, the maximum possible speed with L2CAP + EDL (Extended Data Length) + 15ms interval is 394 kbps.


Please have a look at this comment

The following snippet is taken from there

ble.requestMtu(yourDeviceId, 512, () => {
  console.log('MTU Size ok.');
}, error => {
  console.log('MTU Size failed.');

It is suggesting that you need to request the Mtu after connection and then I think you can break your message into chunks of 512 bytes rather than 20 bytes.

They have done this for android specific issue


First I should say that there are already tons of blog posts and Q&As on the exact same topic, so please read them first.

If you run iPhone 7, you have the LE Data Length Extension. The default MTU is also 185 bytes, which means you can send notifications or write without response commands with 182 bytes of payload. And please make sure you absolutely not use Write With Response or Indications since that will almost stall the transfer. When you run iOS in central mode you are restricted to 30 ms connection interval. Using a shorter connection interval can have benefits, so I would suggest you to run iOS in peripheral mode instead so you from the central side can set a connection interval of something short, say 12 ms. Since iPhone X and iPhone 8, you can also switch to the 2MBit/s PHY to get increased transfer speed. So to answer your actual question why BLE data transfer is handicapped: it’s not, at least if you follow best practice.

You also haven’t told anything about the system that runs your desktop application. If it supports 2 MBit/s PHY, LE Data Length Extension and a MTU of at least 185, then you should be happy and make sure your connections use all those features. If not, you should still get higher performance if you enable at least one of them.