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 read

  • on_characteristic_write(): this method is called when a GATT write operation is requested by a Central device before writing to the destination characteristic’s value

  • on_characteristic_written(): this method is called when a GATT write operation has just been performed, to allow post-processing

  • on_characteristic_subscribed(): this method is called when a Central device has just subscribed to a characteristic for notification or indication

  • on_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

get_mtu() int[source]

Retrieve the connection MTU.

get_pairing_parameters()[source]

Returns the provided pairing parameters, if any.

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

notify_event(event)[source]

Notify event

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

pairing(pairing=None)[source]

Trigger a pairing request with the provided parameters, if any.

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 send

  • conn_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 send

  • conn_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_mtu(mtu: int)[source]

Set connection MTU.

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.

wait_connection(timeout: float = None) bool[source]

Wait for a GATT client to connect to the peripheral. If a connection is already active, returns immediately.

wait_disconnection(timeout: float = None) bool[source]

Wait for a GATT client to disconnect from the peripheral. If no connection is active, returns immediately.

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

class whad.ble.connector.peripheral.PeripheralEventDisconnected(conn_handle: int)[source]

Peripheral disconnected event.

property conn_handle: int

Connection handle

Return type:

int