Sockets and streaming

AbleLib supports peer-to-peer Bluetooth communication via sockets. A socket is a presistent, bi-directional connection to another Bluetooth device, through which data can be exchanged via streams. Sockets can be secure or insecure, depending on the kind of channel the data is being exchanged over.

The basic socket model is abstracted in IAbleSocket, although you'll usually work with models that are more tightly coupled with the channel type, such as IL2CAPSocket for an L2CAP connection. Check out the next section to learn how to use any socket. After that, you can check out this exhaustive list of socket types supported for each platform.

Your device can also host a peer-to-peer channel and accept other sockets by running a server socket. Check out this section to learn more on how to work with server sockets.

Working with sockets

First off, you need to instantiate your socket. All of them will require you to specify if they're secure or not and to provide an AbleDevice, but they may need additional information. This example will make use of an L2CAP socket, which also needs a PSM to identify the channel to connect to.

var socket: IAbleSocket
var device: AbleDevice
var socketConnection: IAbleSocketConnection
...
socket = L2capSocket(secure = true, device, myPsm)

Next, connect the socket. This will return a connection of type IAbleSocketConnection, that can then be used to send and receive data. You can access the connection via the optional connection property of IAbleSocket.

CoroutineScope(Dispatchers.IO).launch {
    try {
        socketConnection = socket.connect()
    } catch (e: AbleSocketException) {
        // exception when connecting or closing
    }
}

Next, define callbacks that will trigger when your socket sends and receives data, respectively. Both of these callbacks are optional and can be changed at any time.

socketConnection.onSend = { result ->
    result.onSuccess { data ->
        // data sent to peer
    }.onFailure { error ->
        // error while sending
    }
}

socketConnection.onReceive = { result ->
    result.onSuccess { data ->
        // data received from peer
    }.onFailure { error ->
        // error while receiving
    }
}

Once the socket is open and connection established, you can send data to the peer. When you're done, close the connection.

CoroutineScope(Dispatchers.IO).launch {
    try {
        socketConnection.send("some text".toByteArray(Charsets.UTF_8))
        socketConnection.close()
    } catch (e: AbleSocketException) {
        // exception when sending or closing
    }
}

Server sockets

A server socket manages a bi-directional communication channel that allows for peer-to-peer communication. It hosts the channel and allows other sockets to connect to it and exchange data with it. Just like client sockets, server sockets are tightly coupled to the channel they operate on.

The basic server socket model is abstracted in IAbleServerSocket, although you'll usually work with models that are more tightly coupled with the channel type, such as IL2CAPServerSocket for an L2CAP connection.

First off, you need to instantiate your server socket. All of them will require you to specify if they're secure or not, but they may need additional information. This example will make use of an L2CAP server socket.

var socket: IAbleServerSocket
...
serverSocket = L2capServerSocket(secure = true)

Next, open the server socket in order to establish the channel for peer-to-peer communication. In particular, L2CAPServerSocket will return an AblePSM corresponding to the PSM of the channel:

CoroutineScope(Dispatchers.IO).launch {
    try {
        val psm = serverSocket.open()
        // prepare for accepting and publish the PSM somewhere
    } catch (e: AbleSocketException) {
        // exception while opening
    }
}

After the server socket is open, you can start accepting incoming client socket connections. After a client socket connects, you'll get back an IAbleSocketConnection that you can use to send and receive data from the client socket.

The server socket doesn't internally store nor manage the accepted connections - it's up to you to hold references to them and close them when the communication is done (or rely on the client socket to do so).

On Android, accept is a blocking suspend function that you call on demand. Once a client socket connects, you can proceed with the communication:

var socketConnection: IAbleSocketConnection
...
CoroutineScope(Dispatchers.IO).launch {
    try {
        // after opening the server socket
        socketConnection = serverSocket.accept().apply {
            onSend = { result ->
                result.onSuccess { data ->
                    // data sent to the client socket
                }.onFailure { error ->
                    // error while sending
                }
            }
            onReceive = { result ->
                result.onSuccess { data ->
                    // received data from client socket
                }.onFailure { error ->
                    // error while receiving
                }
            }
        }
        socketConnection.send("text".toByteArray(Charsets.UTF_8))
    } catch (e: AbleSocketException) {
        // exception while accepting
    }   
}

When the time comes to close the channel and dispose of the server socket, call close. Don't forget to close any open connections to this socket:

CoroutineScope(Dispatchers.IO).launch {
    try {
        serverSocket.close()
    } catch (e: AbleSocketException) {
        // exception while closing
    }
}

Supported socket types

Here's an exhaustive list of all the supported sockets. The list differs between Android and iOS as some socket types aren't supported on a particular platform.

L2CAP

L2CAP sockets are supported starting Android API level 29 (Android Q).

IL2capSocket and its default implementation L2capSocket represent a socket working with an L2CAP channel. Opening this socket requires you to know the PSM of the channel.

socket = L2capSocket(secure = true, device, psm)

Its server socket companion is IL2capServerSocket, with its default implementation being L2capServerSocket. The server socket manages an L2CAP channel and returns its PSM when it opens.

serverSocket = L2capServerSocket(secure = true)

If some bits are still unclear, check out the demo apps (Android and iOS).