Getting started
Scan available devices
Use the whad.ble.connector.scanner.Scanner
class to instanciate
a BLE device scanner and detect all the available devices.
from whad import UartDevice
from whad.ble import Scanner
scanner = Scanner(UartDevice('/dev/ttyUSB0'))
scanner.start()
for rssi, advertisement in scanner.discover_devices():
advertisement.show()
Initiate a connection to a BLE device
Use the whad.ble.connector.central.Central
class to create a
BLE central device and initiate a connection to a BLE peripheral device.
from whad import UartDevice
from whad.ble import Central
# Create a central device
central = Central(UartDevice('/dev/ttyUSB0'))
# Connect to our target device
target = central.connect('0C:B8:15:C4:88:8E')
The connect() method returns a whad.ble.profile.device.PeripheralDevice
object
that represents the remote device.
Enumerate services and characteristics
Once connected, it is possible to discover all the services and characteristics and display them.
# Discover services and characteristics
target.discover()
# Display target profile
print(target)
The whad.ble.profile.device.PeripheralDevice
also provides some methods
to iterate over services and characteristics:
for service in target.services():
print('-- Service %s' % service.uuid)
for charac in service.characteristics():
print(' + Characteristic %s' % charac.uuid)
Read a characteristic
To read a characteristic from an device, just get the corresponding characteristic object and read its value:
charac = device.get_characteristic(UUID('1800'), UUID('2A00'))
if charac is not None:
print('Value: %s' % charac.value)
Write to characteristic
To write a value into a characteristic, this is as simple as reading one:
charac = device.get_characteristic(UUID('1800'), UUID('2A00'))
if charac is not None:
charac.value = b'Something'
Subscribe for notification/indication
Sometimes it is needed to subscribe to notifications or indications for a given
characteristic. This is done through the subscribe() method of whad.ble.profile.device.PeripheralDevice
, as shown below:
def on_charac_updated(characteristic, value, indication=False):
if indication:
print('[indication] characteristic updated with value: %s' % value)
else:
print('[notification] characteristic updated with value: %s' % value)
charac = device.get_characteristic(UUID('1800'), UUID('2A00'))
if charac is not None:
charac.subscribe(
notification=True,
callback=on_charac_updated
)
Close connection
To close an existing connection, simply call the disconnect() method of the whad.ble.profile.device.PeripheralDevice
class:
target.disconnect()
Create a peripheral device
Creating a BLE peripheral device requires to define a custom profile that determines the device services and characteristics:
from whad import UartDevice
from whad.ble import Peripheral
from whad.ble.profile import GattProfile
from whad.ble.profile.advdata import AdvCompleteLocalName, AdvDataFieldList, AdvFlagsField
class MyPeripheral(GenericProfile):
device = PrimaryService(
uuid=UUID(0x1800),
device_name = Characteristic(
uuid=UUID(0x2A00),
permissions = ['read', 'write'],
notify=True,
value=b'TestDevice'
),
null_char = Characteristic(
uuid=UUID(0x2A01),
permissions = ['read', 'write'],
notify=True,
value=b''
),
)
Once this profile defined, instanciate a whad.ble.connector.Peripheral
object
using this profile:
# Instanciate our peripheral
my_profile = MyPeripheral()
# Create a periphal device based on this profile
periph = Peripheral(UartDevice('/dev/ttyUSB0', 115200), profile=my_profile)
# Enable peripheral mode with advertisement data:
# * default flags (general discovery mode, connectable, BR/EDR not supported)
# * Complete local name
periph.enable_peripheral_mode(adv_data=AdvDataFieldList(
AdvCompleteLocalName(b'TestMe!'),
AdvFlagsField()
))
# Start advertising
periph.start()
It is also possible to trigger specific actions when a characteristic is read or written,
through the dedicated callbacks provided by whad.ble.profile.GenericProfile
.
Advanced features
Sending and receiving PDU
It is sometimes useful to send a PDU to a device as well as processing any
incoming PDU without having to use a protocol stack. The BLE whad.ble.connector.Peripheral
and whad.ble.connector.Central
connector provides a nifty way to do it:
from whad.ble import Central
from whad.device import WhadDevice
from scapy.layers.bluetooth4LE import *
# Connect to target
print('Connecting to remote device ...')
central = Central(WhadDevice.create('uart0'))
device = central.connect('00:11:22:33:44:55', random=False)
# Make sure connection has succeeded
if device is not None:
# Disable auto mode
central.auto(False)
# Send a LL_VERSION_PDU
central.send_pdu(BTLE_DATA()/BTLE_CTRL()/LL_VERSION_IND(
version = 0x08,
company = 0x0101,
subversion = 0x0001
))
# Wait for a PDU
while central.is_connected():
pdu = central.wait_pdu()
if pdu.haslayer(LL_VERSION_IND):
pdu[LL_VERSION_IND].show()
break
# Disconnect
device.disconnect()
The above example connects to a target device, sends an LL_VERSION_IND PDU and waits for an LL_VERSION_IND PDU from the remote device.
Normally, when a whad.ble.connector.Central
or whad.ble.connector.Peripheral
connector is used it relies on a protocol stack to handle outgoing and ingoing
PDUs. By doing so, there is no way to get access to the received PDUs and avoid
them to be forwarded to the connector’s internal stack.
However, these connectors expose a method called whad.ble.connector.Central.auto()
that can enable or disable this automatic processing of PDUs. By default, the
PDUs are passed to the underlying protocol stack, but a simple line of code
can disable this behavior:
# Disable automatic PDU processing
central.auto(False)
Once this automatic processing disabled, every received PDU is then stored by
the connector in a dedicated queue, and can be retrieved using a method called
whad.ble.connector.Central.wait_pdu()
. This method is by default synchronous
and will return only when a PDU has been received and put in queue.