Peripheral role
Bluetooth Low Energy peripheral role is used to create a BLE peripheral that accepts
connections from a Central device and exposes a GATT server. WHAD provides a specific
connector to create a BLE device, Peripheral, that implements this role.
A custom GATT profile class derived from whad.ble.profile.GenericProfile
needs to be defined and provided as a parameter when instantiating a Peripheral
object.
The Peripheral connector allows to register a single event listener of class
PeripheralEventListener through its
Peripheral.attach_event_listener() method. This event listener must
be created with a callback function attached that will be called by the connector to
notify any connection or disconnection event (respectively a
whad.ble.connector.peripheral.PeripheralEventConnected and
whad.ble.connector.peripheral.PeripheralEventDisconnected instance).
Important
The mechanism used to handle asynchronous events like connection and disconnection is still a work in progress. It has been introduced in a recent update to better handle connection and disconnection events and is not yet intended to be used by anything other WHAD’s internal code.
We have a full rework of WHAD’s internals planned, including BLE Central and Peripheral classes, that will definitely bring some changes to the way connectors work. We will do our best not to break the current implementation.
Creating a basic Bluetooth Low Energy Peripheral device
First, a custon GATT profile is defined as a class deriving from
Profile in which each service, its associated
characteristics and descriptors will be defined as default class properties.
These properties will be used to populate the corresponding GATT server attributes
database with each user-defined services, characteristics and descriptors.
The following code defines a simple GATT profile (defined by the 16-bit UUID 0x1800) defining a Generic Access Service with its associated DeviceName characteristic (defined by the 16-bit UUID 0x2A00):
from whad.ble.profile import UUID, Profile, PrimaryService, Characteristic
class CustomProfile(Profile):
"""Custom GATT profile"""
# Define a generic access service (GAS) with UUID 0x1800
gas = PrimaryService(
UUID(0x1800),
# Define a DeviceName characteristic with read/write permissions
device_name = Characteristic(
UUID(0x2A00),
# Read/write/notify permissions
permissions=['read', 'write', 'notify'],
# Default value for this characteristic
value=b"TestDevice"
)
)
This custom GATT profile does not follow the Bluetooth Specifications and especially its default Generic Access Profile (as defined in Vol 3, Part C, Section 12).
It is then possible to create an instance of Peripheral using this custom
GATT profile class and specific advertising data, as follows:
from whad.device import Device
from whad.ble import (
Peripheral, AdvCompleteLocalName, AdvDataFieldList, AdvFlagsField, UUID,
PrimaryService, Characteristic, GenericProfile,
)
class CustomProfile(Profile):
"""Custom GATT profile"""
# Define a generic access service (GAS) with UUID 0x1800
gas = PrimaryService(
UUID(0x1800),
# Define a DeviceName characteristic with read/write permissions
device_name = Characteristic(
UUID(0x2A00),
# Read/write permissions
permissions=['read', 'write'],
# Characteristic supports notifications with a
# ClientCharacteristicConfiguration descriptor (CCCD)
notify=True,
# Default value for this characteristic
value=b"TestDevice"
)
)
# Create an instance of Peripheral class using HCI device hci0 and a custom
# profile defined in CustomProfile
profile = CustomProfile()
peripheral = Peripheral(
Device.create("hci0"),
profile=profile,
adv_data=AdvDataFieldList(
AdvFlagsField(), # Defines a default Flags record
AdvCompleteLocalName(b"TestDevice") # Adds a CompleteLocalName record
)
)
Starting and using a custom Peripheral device
The previously created peripheral is completely defined and ready to be started
by calling its Peripheral.start() method:
peripheral.start()
WHAD will advertise a Bluetooth Low Energy Peripheral device using the specified advertising data, wait for a connection from a Central device and once established will expose a GATT server interacting with the custom profile associated with it.
The peripheral’s characteristics’s values can be accessed from both a remotely connected Central device and the main application that created this peripheral device. These two possibilities are detailed hereafter.
Updating a custom profile’s characteristic value from the main application
Changing a characteristic value from the main application is possible by simply
using the profile instance passed to the Peripheral’s constructor.
In the custom profile defined above, the main application can access the DeviceName characteristic by using the dynamically populated properties, as shown below:
profile.gas.device_name.value = b"NewDeviceName"
Writing into the mapped characteristic value object causes the associated GATT attribute to be updated with the provided value. If the corresponding characteristic accepts notifications and the GATT client connected to the peripheral device has subscribed for notifications, a notification is automatically sent by the Peripheral device to the connected Central devicem no matter if the written value differs or not from the previous one. If a Central device has subscribed for indications, an indication is sent to the Central device instead of a notification.
The above example relies on the fact the GATT profile class defines its characteristic using WHAD’s Device Model feature (see Device Model), but some profile instances are created either from a JSON profile or dynamically populated, with no specific property defined. In this case, accessing a characteristic and updating its value is a bit more complex:
generic_service = profile.service('1800')
if generic_service is not None:
dev_name = battery_service.char('2a00'))
dev_name.value = b"NewDeviceName"
If the Central device has subscribed for notifications or indications, a notifcation or indication will be sent once the characteristic’s value has been modified.
Reacting on specific GATT events for a service’s characteristic
Any custom GATT profile class inheriting from Profile
can use a set of specifically designed decorators to declare a method as a handler
for a GATT event related to a specific characteristic. The read
decorator for instance can be used to intercept any read operation on a specific characteristic,
like in the following example code:
from whad.device import Device
from whad.ble import (
Peripheral, Profile, read, BatteryService, AdvCompleteLocalName,
AdvDataFieldList, AdvFlagsField,
)
class BatteryDevice(Profile):
"""Device exposing a battery service
"""
battery = BatteryService()
@read(battery.level)
def on_battery_level_read(self, offset, length):
level = self.battery.percentage - 10
if level <= 0:
level = 100
self.battery.percentage = level
return self.battery.level.value
In this example code, we define a new BatteryDevice class inheriting
from Profile and add a standard Battery Service
defined by BatteryService. This
generic service adds its own characteristics as specified in
the Bluetooth Battery Service specification
as well as dedicated properties to retrieve and set the corresponding battery
level as a percentage.
A custom GATT read event handler is defined for this profile’s battery level
characteristic (identified by UUID 0x2A19 within the corresponding battery service
identified by UUID 0x180F), thanks to the read decorator.
The argument passed to this decorator is declared within the BatteryService
class as level and can be used to identify a specific
characteristic belonging to the GATT model defined within any Profile
class. Basically, any characteristic defined in a GATT profile class can be passed to this
decorator.
The decorated method, on_battery_level_read() accepts two parameters specifying the
offset and length required by the GATT read operation and shall be used to return
any partial value required by a Central device. In this example, we don’t care about
any offset or length because most of the Central devices will read this characteristic
without using a GATT LongRead procedure (the characteristic value is stored on a single
byte), but a better implementation would take care of it to properly handle errors.
In our implementation, we first retrieve the current battery level from the characteristic’s
value, decrements this value by 10 (setting it back to 100 if it reaches 0 or below),
write this value into the characteristic’s value and return the updated characteristic value.
This method is always called before WHAD’s BLE stack returns any value to the Central
device that initiated this GATT read operation, allowing to change the behavior of the
peripheral when needed.
In case a GATT profile has been dynamically populated from a JSON profile file, overriding
Profile’s GATT operation handlers is the best way to
intercept any operation and modify the profile instance accordingly. The following methods
can be overriden to intercept different GATT operations:
on_characteristic_read(): this method is called when a GATT read operation is about to be peformed by WHAD’s BLE stack and is in charge of calling any registered handler regarding the characteristic that is about to be readon_characteristic_write(): this method is called when a GATT write operation is requested by a Central device before writing to the destination characteristic’s valueon_characteristic_written(): this method is called when a GATT write operation has just been performed, to allow post-processingon_characteristic_subscribed(): this method is called when a Central device has just subscribed to a characteristic for notification or indicationon_characteristic_unsubscribed(): this method is called when a Central device has just unsubscribed from a characteristic
If one or many of these methods are redefined (overriden) in a child class inheriting from
Profile, calling the parent class implementation with super()
is mandatory:
class MyChildClass(Profile):
def on_characteristic_read(self, service: Service, characteristic: Characteristic, offset: int = 0, length: int = 0):
"""Hook for GATT read operation"""
# Call parent method
super().on_characteristic_read(service, characteristic, offset=offset, length=length)
# Continue with custom processing (Forcing characteristic value)
raise HookReturnValue(b"Oops")
Python API for Peripheral Role
The Bluetooth Low Energy peripheral role is provided by the Peripheral connector class, inheriting from
the BLE default connector.
Bluetooth Low Energy Peripheral connector
- class whad.ble.connector.peripheral.Peripheral(device, existing_connection=None, profile=None, adv_data=None, scan_data=None, bd_address=None, public=True, stack=<class 'whad.ble.stack.BleStack'>, gatt=<class 'whad.ble.stack.gatt.GattServer'>, pairing=<whad.ble.stack.smp.parameters.Pairing object>, security_database=None)[source]
This BLE connector provides a way to create a peripheral device.
A peripheral device exposes some services and characteristics that may be accessed by a central device. These services and characteristics are defined by a specific profile.
- attach_event_listener(listener: PeripheralEventListener)[source]
Attach an event queue to receive asynchronous notifications.
- Parameters:
listener (PeripheralEventListener) – Event listener
- property conn_handle: int
Connection handle
- property gatt
Generic Attribute layer
- is_connected() bool[source]
Determine if the peripheral has an active connection from a GATT client.
- property listener
Attached event listener
- property local_peer
Local peer object
- on_connected(connection_data)[source]
A device has just connected to this peripheral.
- Parameters:
connection_data (
whad.protocol.ble_pb2.Connected) – Connection data
- on_ctl_pdu(pdu)[source]
This method is called whenever a control PDU is received. This PDU is then forwarded to the BLE stack to handle it.
Peripheral devices act as a slave, so we only forward master to slave messages to the stack.
- Parameters:
pdu (
scapy.layers.bluetooth4LE.BTLE) – BLE PDU
- on_data_pdu(pdu)[source]
This method is called whenever a data PDU is received. This PDU is then forwarded to the BLE stack to handle it.
- Parameters:
pdu (
scapy.layers.bluetooth4LE.BTLE_DATA) – BLE PDU
- on_disconnected(disconnection_data)[source]
A device has just disconnected from this peripheral.
- Parameters:
connection_data (
whad.protocol.ble_pb2.Disconnected) – Connection data
- on_new_connection(connection)[source]
On new connection, discover primary services
- Parameters:
connection (
whad.protocol.ble_pb2.Connected) – Connection data
- property profile: GenericProfile | None
GATT Profile
- property remote_peer
Remote peer object
- property security_database
Security Database
- send_ctrl_pdu(pdu, conn_handle=1, direction=2, access_address=2391391958, encrypt=None) bool[source]
Send a PDU to the central device this peripheral device is connected to.
Sending direction is set to ̀ BleDirection.SLAVE_TO_MASTER` as we need to send PDUs to a central device.
- Parameters:
pdu (
scapy.layers.bluetooth4LE.BTLE) – PDU to sendconn_handle (int) – Connection handle
direction (
whad.protocol.ble_pb2.BleDirection, optional) – Sending direction (to master or slave)access_address (int, optional) – Target access address
- Returns:
PDU transmission result.
- Return type:
bool
- send_data_pdu(data, conn_handle=1, direction=2, access_address=2391391958, encrypt=None) bool[source]
Send a PDU to the central device this peripheral device is connected to.
Sending direction is set to ̀ BleDirection.SLAVE_TO_MASTER` as we need to send PDUs to a central device.
- Parameters:
pdu (
scapy.layers.bluetooth4LE.BTLE) – PDU to sendconn_handle (int) – Connection handle
direction (
whad.protocol.ble_pb2.BleDirection, optional) – Sending direction (to master or slave)access_address (int, optional) – Target access address
- Returns:
PDU transmission result.
- Return type:
bool
- set_profile(profile: GenericProfile)[source]
Set peripheral profile.
- property smp
Security Manager Protocol
- use_stack(clazz=<class 'whad.ble.stack.BleStack'>)[source]
Specify a stack class to use for BLE. By default, our own stack (BleStack) is used.
- Parameters:
clazz (
whad.ble.stack.BleStack) – BLE stack to use.
Bluetooth Low Energy Peripheral events
- class whad.ble.connector.peripheral.PeripheralEventConnected(conn_handle: int, local: BDAddress, remote: BDAddress)[source]
Connected event
- property conn_handle: int
Connection handle
- Return type:
int
- property local: BDAddress
Peripheral BD address
- Return type:
whad.ble.BDAddress
- property remote: BDAddress
Central BD address
- Return type:
whad.ble.BDAddress