Communication Guide¶
We modeled the communications in ABle after Berkeley Sockets, so ABleCentrals can send() and recv() data over characteristics. The way we do this is by specifying a characteristic to receive from and any data written to that is placed in its respective ‘characteristic queue’ for the central who wrote it to be retrieved the next time recv() is called. “Sending” is achieved through putting data on a characteristic and only notifying or indicating the central that the data is being sent to (if you’re on Linux, this leverages a custom addition to Bluez, see the top level README).
Note
Sending requires the central to already be subscribed to notifications or indications after it connects.
Setup¶
At this point in our guide we have configured a gatt table with a service and a characteristic, added an advertisement and are already accepting then disconnecting from centrals. We are going to add some code in template to be able to receive and send data to a central. Before we send and receive data we need to mark a characteristic for default communications, this will be the characteristic that the ‘sockets’ communicate over by default, think of it as a transport layer. This is achieved as adding a param when the characteristic is added to the peripheral.
able_peripheral.add_characteristic(service, new_char, is_comms_char=True)
This will mark the characteristic added in the GATT Table guide as the communications characteristic.
Receiving¶
The way that the BLE central “sockets” receive data in ABle is triggered whenever a given central writes a characteristic. ABle detects the write and adds the data that was written to the characteristic to a queue that is associated with that characteristic and that central (since the central connected, it already is abstracted as ABleCentral) and the recv() function blocks on getting data from that characteristic queue.
To try this yourself, the central will write data to the marked characteristic after it is connected. Alternatively, the central will write data to a different characteristic. To receive data on a characteristic other than the marked one, we just need to pass the characteristic as an argument to recv(). We recommend using nRFConnect on iOS or Android to test this. Replace the code from the connection guide with the following block which will accept the connection then wait 60 seconds for it to write to the communications characteristic and alternative characteristic.
# Create alternative characteristic if not already created
characteristic_alt = ABleCharacteristic(characteristic_uuid=uuid.uuid4())
able_peripheral.add_characteristic(service, characteristic_alt)
new_central = await able_peripheral.accept()
try:
logger.info("Waiting for data from the central on marked comms characteristic...")
data = await new_central.recv(timeout=60)
logger.info(f"Got {data} from {new_central}")
logger.info("Waiting for data from the central on alternate characteristic"...)
data = await new_central.recv(characteristic_alt, timeout=60)
except:
logger.exception("Timed out on sending or receiving data")
finally:
await new_central.close()
Make sure you can run this and write to the characteristic using your central and that the data is logged.
Clearing a Centrals Recv Buffer¶
There are scenarios where you may want to clear the recv queue for a central before starting a new communication, for that use the sync method flush_buffer shown below:
central.flush_buffer()
Sending¶
Sending to a BLE central is made possible through notifications and indications from BLE, the main way a peripheral can notify a central that data has changed on a characteristic. After the central has subscribed to notifications from the peripheral, we can send data to a single central by setting the value of a characteristic then notifying just the central we want to send data to of the change, giving us a pseudo-send.
To test this yourself, remember that before sending data your central must be subscribed to notifications/indications on the characteristic we marked before after it connects (if using send() without extra params, since it defaults to the communications characteristic). That way once we accept the central instance it is ready to have data sent to it, the following code block includes a 15 second sleep so there is time to subscribe after connecting.
new_central = await able_peripheral.accept()
logger.info("Subscribe to notifications to the marked characteristic and/or any other characteristics you are sending on!")
await asyncio.sleep(15)
try:
await new_central.send(b'1234')
await new_central.send(b'12345', characteristic_alt)
except:
logger.exception("Errored out on sending data")
finally:
await new_central.close()
Warning
If a RuntimeWarning or RuntimeError is raised, this is likely because the central did not subscribe to notifications in time.
If you choose you can pass in another instance of ABleCharacteristic as the second param to send and the data will be ‘sent’ on that characteristic by updating its value and notifying the central.