Device Model
WHAD uses a specific device model to create BLE peripherals. This device model
is implemented in Profile and allows dynamic
modification of services and characteristics but also provides a convenient
way to define a device’s services, characteristics and descriptors.
Creating a device model of a BLE peripheral
Here is an example of a BLE peripheral implemented with WHAD:
from whad.ble import UUID, Profile, PrimaryService, Characteristic
class MyPeripheral(Profile):
generic_access = PrimaryService(
UUID(0x1800),
device_name = Characteristic(
UUID(0x2A00),
permissions=['read', 'notify'],
value=b'My device name'
)
)
Profile performs an introspection on its properties
to find every instance of PrimaryService, finds every
instance of Characteristic declared into each service
and populates its attribute database based on the discovered information.
But this mechanism also allows dynamic modification of any characteristic, for instance the device name characteristic:
periph_inst = MyPeripheral()
periph_inst.generic_access.device_name = b'Another name'
Of course, this can also be done when the peripheral is running and will cause the BLE stack to send notifications or indications based on the characteristics properties.
Defining and using a standard service
WHAD provides a StandardService class used to
define standard services in a custom profile. This class is a wrapper around the
generic PrimaryService that automatically retrieves the service’s UUID
from a special class property and equip the service class with additional methods
allowing to provide an abstraction level from its characteristics.
A standard service shall be defined as follows:
from struct import pack, unpack
from whad.ble import UUID, Characteristic
from whad.ble.service import StandardService
class CustomService(StandardService):
"""An example of a standard service.
This service exposes a single read-only characteristic
that contains some version number stored on 3 bytes.
It defines a property named `version` that parses
the characteristic's value and return a tuple with
the version's major, minor and revision numbers.
When this property is set, the provided version numbers
are packed into a byte array and written into the
corresponding characteristic's value.
"""
# Service UUID is expected in `_uuid`:
_uuid = UUID(0x1337)
# Characteristics are defined as class properties
version = Characteristic(
UUID(0x1338),
properties=Characteristic.READ,
value=b'\x01\x00\x00',
required=True, # Characteristic MUST be present
)
@property
def version(self) -> (int, int, int):
return unpack("<BBB", self.version.value)
@version.setter
def version(self, version: (int, int, int)):
major, minor, rev = version
self.version.value = pack('<BBB', major, minor, rev)
The same class will be used to define a peripheral’s profile and access the service from a central device. The following code defines a peripheral profile using the above standard service:
from whad.ble import Profile, Peripheral
from .service import CustomService
class CutomProfile(Profile):
"""My custom profile"""
custom_service = CustomService()
# Create an instance of our profile and set its version
profile = CustomProfile()
profile.custom_service.version = (1,2,3)
# Create a peripheral using this profile
periph = Peripheral(Device.create('hci0'), profile=profile)
And to access the same service from a GATT client:
from whad.ble import Profile, Central
from .service import CustomService
class CutomProfile(Profile):
"""My custom profile"""
custom_service = CustomService()
# Create a central device
central = Central(Device.create('hci0'))
# Connect to a device that exposes our custom service
target = central.connect('11:22:33:44:55:66')
if target is not None:
# Once connected, query the service and read the version
if target.has(CustomService):
custom_service = target.query(CustomService)
major, minor, rev = custom_service.version
print(f"Device custom version: {major}.{minor}.{rev}")
else:
print("Device does not expose our custom service.")
# Disconnect
target.disconnect()
Supported standard services
The following standard Bluetooth Low Energy services are supported:
Hooking GATT events on characteristics
WHAD BLE device model provides a set of method decorators that must be used to attach a method to a specific event and a specific characteristic:
readdeclares a characteristic read event handlerwritedeclares a characteristic before-write event handlerwrittendeclares a characteristic after-write event handlersubscribeddeclares a characteristic subscribe event handlerunsubscribeddeclares a characteristic unsubscribe event handler
A characteristic event handler may raise one of the following exception to cause the GATT stack to react accordingly:
HookReturnValue: force a characteristic value to be returned to a GATT client on a read eventHookReturnGattError: generates a GATT error that will be sent back to the connected GATT clientHookReturnNotFound: tells a GATT client the characteristic does not existHookReturnAccesDenied: tells a GATT client that authentication is required to access this characteristic
If no exception is raised in the event handler, the GATT operation continues as expected. As an example, here follows a peripheral model declaration that uses a characteristic event handler:
class MyPeripheral(Profile):
generic_access = PrimaryService(
UUID(0x1800),
device_name = Characteristic(
UUID(0x2A00),
permissions=['read', 'write', 'notify'],
value=b'My device name'
)
)
@read(generic_access.device_name)
def on_device_name_read(self, offset, mtu):
"""Return the content of the device name characteristic prefixed with 'FOO'
"""
raise HookReturnValue(b'FOO'+ self.generic_access.device_name.value)
@written(generic_access.device_name)
def on_device_name_changed(self, value, without_response):
"""Called every time the device name characteristic has been changed by client.
"""
print(f"Device name has been changed to: {value}")
GATT Profile API
- class whad.ble.read(*args)[source]
Read hook decorator
This decorator is used to declare a callback method for read operations on a specific characteristic.
- class whad.ble.write(*args)[source]
Write hook decorator
This decorator is used to declare a callback method for write operations on a specific characteristic. This callback will be called before the write operation happens.
- class whad.ble.written(*args)[source]
Written hook decorator
This decorator is used to declare a callback method for write operations on a specific characteristic. This callback will be called after the write operation happens.
- class whad.ble.subscribed(*args)[source]
Subscribe hook decorator
This decorator is used to declare a callback method for subscribe operations on a specific characteristic.
- class whad.ble.unsubscribed(*args)[source]
Unsubscribe hook decorator
This decorator is used to declare a callback method for unsubscribe operations on a specific characteristic.
- class whad.ble.Profile(start_handle: int = 1, from_json: str | None = None)[source]
This class implements a GATT profile, i.e. a set of services and characteristics exposed by a Bluetooth Low Energy GATT server.
- __init__(start_handle: int = 1, from_json: str | None = None)[source]
Parse the device model, instantiate all the services, characteristics and descriptors, compute all handle values and registers everything inside this instance for further use.
- Parameters:
start_handle (int) – Start handle value to use (default: 0)
from_json (str) – JSON data describing a GATT profile
- add_service(service: Service, handles_only: bool = False)[source]
Add a service to the current device
- Parameters:
service (Service) – Service to add to the device
handles_only (bool) – Add only service handles if set to
True
- attr_by_type_uuid(uuid, start: int = 1, end: int = 65535) Iterator[Attribute][source]
Enumerate attributes that have a specific type UUID.
- Parameters:
uuid (UUID) – Type UUID
start (int) – Start handle
end (int) – End handle
- char(uuid: str | UUID) Characteristic | None[source]
Get characteristic by its UUID.
- Parameters:
uuid (str) – Characteristic UUID to look for
- Returns:
Characteristic if found,
Noneotherwise- Return type:
- export_json() str[source]
Export profile as JSON data, including services, characteristics and descriptors definition.
- Returns:
JSON data corresponding to this profile
- Return type:
str
- find_characteristic_by_value_handle(value_handle: int) Characteristic | None[source]
Find characteristic object by its value handle.
- Parameters:
value_handle (int) – Characteristic value handle
- Returns:
Corresponding characteristic object or
Noneif not found.- Return type:
- find_characteristic_end_handle(handle: int) int | None[source]
Find characteristic end handle based on its handle.
- Parameters:
handle (int) – Characteristic handle
- Return type:
int
- Returns:
Characteristic value handle
- Raises:
InvalidHandleValueException
- find_hook(service: Service, characteristic: Characteristic, operation: str) Callable[[...], Any] | None[source]
Find a registered hook for a specific service, characteristic and operation.
- Parameters:
service (Service) – Service object
characteristic (Characteristic) – Characteristic object
operation (str) – GATT operation
- Returns:
Hook callback
- Return type:
callable
- find_object_by_handle(handle: int) Attribute[source]
Find an object by its handle value
- Parameters:
handle (int) – Object handle
- Returns:
Object if handle is valid, or raise an IndexError exception otherwise
- Return type:
Attribute
- Raises:
IndexError
- find_objects_by_range(start: int, end: int) List[Attribute][source]
Find attributes with handles belonging in the [start, end+1] interval.
- Parameters:
start (int) – Start handle value
end (int) – End handle value
- Returns:
List of objects with handles between start and end values
- Return type:
list
- Raises:
IndexError
- find_service_by_characteristic_handle(handle: int) Service[source]
Find a service object given a characteristic handle that belongs to this service.
- Parameters:
handle (int) – Characteristic handle belonging to the searched service
- Return type:
- Returns:
Service object containing the specified characteristic
- Raises:
InvalidHandleValueException
- get_characteristic_by_uuid(uuid: str | UUID)[source]
Get characteristic by its UUID.
- Parameters:
uuid (str) – Characteristic UUID to look for
- Returns:
Characteristic if found,
Noneotherwise- Return type:
- Raises:
InvalidUUIDException – Specified UUID is invalid
- get_service_by_uuid(uuid: str | UUID) Service | None[source]
Retrieve a Service object given its UUID.
- Parameters:
uuid (str) – Service UUID
- Returns:
Corresponding Service object if found, None otherwise.
- Return type:
Service, optional
- Raises:
InvalidUUIDException – Specified UUID is invalid
Deprecated since version 1.3.0: Use the
service()method to find a service based on its UUID (simpler syntax).
- included_services() Iterator[IncludeService][source]
Enumerate included services.
- on_characteristic_read(service: Service, characteristic: Characteristic, offset: int = 0, length: int = 0)[source]
Characteristic read hook.
This hook is called whenever a characteristic is about to be read by a GATT client. If this method returns a byte array, this byte array will be sent back to the GATT client. If this method returns None, then the read operation will return an error (not allowed to read characteristic value).
- Parameters:
service (Service) – Service owning the characteristic
characteristic (Characteristic) – Characteristic object
offset (int) – Read offset (default: 0)
length (int) – Max read length
- Returns:
Value to return to the GATT client
- Return type:
bytes
- on_characteristic_subscribed(service: Service, characteristic: Characteristic, notification: bool = False, indication: bool = False)[source]
Characteristic subscribed hook
This hook is called whenever a characteristic has been subscribed to.
- Parameters:
service (Service) – Service owning the characteristic
characteristic (Characteristic) – Characteristic object
notification (bool) – Set to
Trueif subscribed to notificationindication (bool) – Set to
Trueif subscribed to notification
- on_characteristic_unsubscribed(service: Service, characteristic: Characteristic)[source]
Characteristic unsubscribed hook
This hook is called whenever a characteristic has been unsubscribed.
- Parameters:
service (Service) – Service owning the characteristic
characteristic (Characteristic) – Characteristic object
- on_characteristic_write(service: Service, characteristic: Characteristic, offset: int = 0, value: bytes = b'', without_response: bool = False)[source]
Characteristic write hook
This hook is called whenever a charactertistic is about to be written by a GATT client.
- Parameters:
service (Service) – Service owning the characteristic
characteristic (Characteristic) – Characteristic object
offset (int) – Read offset (default: 0)
value (bytes) – Value about to be written into the characteristic
without_response (bool) – Set to
Trueif no response is required
- on_characteristic_written(service: Service, characteristic: Characteristic, offset: int = 0, value: bytes = b'', without_response: bool = False)[source]
Characteristic written hook
This hook is called whenever a charactertistic has been written by a GATT client.
- Parameters:
service (Service) – Service owning the characteristic
characteristic (Characteristic) – Characteristic object
offset (int) – Read offset (default: 0)
value (bytes) – Value about to be written into the characteristic
without_response (bool) – Set to
Trueif no response is required
- on_connect(conn_handle: int)[source]
Connection hook.
This hook is only used to notify the connection of a device.
- Parameters:
conn_handle (int) – Connection handle
- on_disconnect(conn_handle: int)[source]
Disconnection hook.
This hook is only used to notify the disconnection of a device.
- Parameters:
conn_handle (int) – Connection handle
- on_indication(service: Service, characteristic: Characteristic, value: bytes)[source]
Characteristic indication hook.
This hook is called when a indication is sent to a characteristic.
- Parameters:
service (Service) – Service owning the characteristic
characteristic (Characteristic) – Characteristic object
value (bytes) – Characteristic value
- on_notification(service: Service, characteristic: Characteristic, value: bytes)[source]
Characteristic notification hook.
This hook is called when a notification is sent to a characteristic.
- Parameters:
service (Service) – Service owning the characteristic
characteristic (Characteristic) – Characteristic object
value (bytes) – Characteristic value
- register_attribute(attribute: Attribute)[source]
Register a GATT attribute
- Parameters:
attribute (Attribute) – Attribute to register
- remove_service(service: Service, handles_only: bool = False)[source]
Remove service
- Parameters:
service (Service) – Service object or UUID
handles_only (bool) – Remove only handles if set to
True
- services() Iterator[Service][source]
Enumerate service objects.
This method is a generator and will yield service objects registered into the profile.
- update_service(service: Service) bool[source]
Update service in profile.
Keep service in place in the service list, but update all the services declared after this one.
- Parameters:
service (Service) – Service object to update.
- Returns:
Trueif service has been updated,Falseotherwise.- Return type:
bool
- class whad.ble.Service(type_uuid: UUID, uuid: UUID, handle: int = 0, end_handle: int = 0, **children)[source]
GATT service attribute
- __init__(type_uuid: UUID, uuid: UUID, handle: int = 0, end_handle: int = 0, **children)[source]
Instantiate a new service attribute with the specified handle.
- add_characteristic(characteristic: Characteristic)[source]
Add characteristic into this service.
- Parameters:
characteristic (Characteristic) – Characteristic object to add into this service.
- add_included_service(included_service: IncludeService)[source]
Add include service definition.
- Parameters:
included_service (IncludeService) – Include service definition
- char(uuid: str | UUID) Characteristic | None[source]
Get characteristic by UUID.
- Parameters:
uuid (UUID) – Searched characteristic’s UUID
- Returns:
Characteristic object if found, None otherwise
- Return type:
Characteristic, optional
- Raises:
InvalidUUIDException – Invalid UUID
- characteristics() Iterator[Characteristic][source]
Enumerate characteristics.
- Returns:
Iterator over this service’s characteristics.
- property end_handle: int
End handle
- get_characteristic(uuid: str | UUID) Characteristic | None[source]
Get characteristic by UUID.
Deprecated since version 1.3.0: Use
char()instead ofget_characteristic().- Parameters:
uuid (UUID, str) – Searched characteristic’s UUID
- Returns:
Characteristic object if found, None otherwise
- Return type:
Characteristic, optional
- Raises:
InvalidUUIDException – Invalid UUID
- property handle: int
Attribute handle
- included_services() Iterator[IncludeService][source]
Enumerate included services
- property name: str
Readable service name
- Returns:
Service name as readable text, if either defined in the Bluetooth specification or known from DarkMentorLLC’s CLUES database.
- Return type:
str
- payload()[source]
Return service attribute’s value.
- Returns:
Service attribute value
- Return type:
bytes
- remove_characteristic(characteristic: UUID | Type[Characteristic])[source]
Remove a specific characteristic
- Parameters:
characteristic (Characteristic, UUID) – Characteristic object to remove from service’s characteristics.
- remove_include_service(included_service: UUID | Type[IncludeService])[source]
Remove a specific include service definition.
- Parameters:
included_service (IncludeService, UUID) – Include service definition or its UUID
- class whad.ble.PrimaryService(uuid: UUID, handle: int = 0, end_handle: int = 0, **characteristics)[source]
Primary service attribute.
This attribute has a type UUID of 0x2800.
- __init__(uuid: UUID, handle: int = 0, end_handle: int = 0, **characteristics)[source]
Initialize a primary service of UUID uuid and declare the requested characteristics.
- Parameters:
uuid (UUID) – Service UUID
handle (int, optional) – Service handle
end_handle (int, optional) – Handle of service’s last attribute
characteristics (dict) – Additional characteristics’s definitions to add into this service
- class whad.ble.SecondaryService(uuid, handle: int = 0)[source]
Secondary service attribute.
This attribute has a type UUID of 0x2801.
- class whad.ble.IncludeService(uuid, handle=0, start_handle=0, end_handle=0)[source]
IncludeService Attribute class
This class stores the information related to an included service:
the included service UUID (16-bit or 128-bit UUID)
the start and end handles of the said included service
- __init__(uuid, handle=0, start_handle=0, end_handle=0)[source]
Initialize an included service
- Parameters:
uuid (UUID) – Included service UUID
handle (int) – Included service start handle
end_handle (int) – Included service end handle
- property end_handle
Return this attribute end handle
This attribute does not belong to a group so its end handle is the same than its handle.
- property name: str
Generate the description of the included service definition attribute.
- property service_end_handle: int
Return the included service end handle
- property service_start_handle: int
Return the included service start handle
- class whad.ble.Characteristic(uuid: UUID, handle: int = 0, end_handle: int = 0, value: bytes = b'', properties: int = 0, permissions: List[str] | None = None, notify: bool = False, indicate: bool = False, required: bool = False, description: str | None = None, security: SecurityAccess | None = None, descriptors: List[Descriptor] = [])[source]
BLE Characteristic
- __init__(uuid: UUID, handle: int = 0, end_handle: int = 0, value: bytes = b'', properties: int = 0, permissions: List[str] | None = None, notify: bool = False, indicate: bool = False, required: bool = False, description: str | None = None, security: SecurityAccess | None = None, descriptors: List[Descriptor] = [])[source]
Instantiate a BLE characteristic object
- Parameters:
uuid – 16-bit or 128-bit UUID
handle (int) – Handle value
value (bytes) – Characteristic value
perms (int) – Permissions
- add_descriptor(descriptor: Descriptor) Characteristic[source]
Add a descriptor
- Parameters:
descriptor (
whad.ble.profile.characteristic.Descriptor) – Descriptor instance to add to this characteristic.
- property alias: str | None
Characteristic alias.
- attach(service)[source]
Attach this characteristic to a service.
- Parameters:
service (Service) – Service referenced
- can_indicate() bool[source]
Determine if characteristic sends indications.
- Returns:
Trueif characteristic sends indication,Falseotherwise.- Return type:
bool
- can_notify() bool[source]
Determine if characteristic sends notifications.
- Returns:
Trueif characteristic sends notification,Falseotherwise.- Return type:
bool
- check_security_property(access_type, prop)[source]
Returns a boolean indicating if a property is required for a given access type.
- Parameters:
access_type (SecurityAccess) – access type to check
property (SecurityProperty) – security property to check
- descriptors() Iterator[Descriptor][source]
Iterate over the registered descriptors (generator)
- property end_handle
Characteristic end handle
- get_descriptor(desc_type: UUID | Type[Descriptor]) Descriptor | None[source]
Retrieve a decriptor based on its type UUID or class.
- Parameters:
desc_type (UUID,
whad.ble.profile.characteristic.Descriptor) – Descriptor type- Returns:
First matching descriptor belonging to this characteristic
- Return type:
whad.ble.profile.Descriptor, optional
- get_security_access(access_type)[source]
Returns the security access properties linked to an access type.
- Parameters:
access_type (SecurityAccess) – access type to check
- property handle: int
Attribute handle
- must_indicate()[source]
Determine if an indication must be sent for this characteristic.
Indication must be sent when a characteristic has the indication property and its ClientCharacteristicConfiguration descriptor has indications enabled.
- must_notify()[source]
Determine if a notification must be sent for this characteristic.
Notification must be sent when a characteristic has the notification property and its ClientCharacteristicConfiguration descriptor has notifications enabled.
- property name
Characteristic standard name (if any)
- property properties
Characteristic properties
- property security: SecurityAccess
Returns security access property
- property service
Service this characteristic belongs to.
- property value
Attribute value
- property value_attr
Associated value attribute
- property value_handle
Characteristic value handle
- class whad.ble.Descriptor(uuid: UUID, handle: int = 0, value: bytes = b'', characteristic: Characteristic | None = None)[source]
BLE Characteristic descriptor
- __init__(uuid: UUID, handle: int = 0, value: bytes = b'', characteristic: Characteristic | None = None)[source]
Instantiate a GATT Attribute
- property characteristic: Characteristic | None
Parent characteristic
- static from_uuid(characteristic, handle: int, uuid: UUID, value: bytes)[source]
Create an instance of a descriptor based on the provided UUID and descriptor value.
- Parameters:
uuid (UUID) – Descriptor UUID
value (bytes) – Descriptor value
- Returns:
Instance of the corresponding descriptor
- Return type:
- static get_type_uuid(desc_cls) UUID | None[source]
Find the type UUID corresponding to a given registered descriptor’s class.
- Returns:
Descriptor’s type UUID if found, None otherwise.
- Return type:
- property name
Descriptor name
- static register_type(uuid: UUID, cls)[source]
Register descriptor type (associate a descriptor UUID with the corresponding Python class (must inherit from Descriptor)
- property uuid
Descriptor UUID
- class whad.ble.ClientCharacteristicConfig(handle: int = 0, notify: bool = False, indicate: bool = False, characteristic: Characteristic | None = None)[source]
Client Characteristic Configuration Descriptor
- __init__(handle: int = 0, notify: bool = False, indicate: bool = False, characteristic: Characteristic | None = None)[source]
Instantiate a Client Characteristic Configuration Descriptor
- Parameters:
notify (bool) – Set to True to get the corresponding characteristic notified on change
indicate (bool) – Set to True to get the corresponding characteristic indicated on change
- property config
CCCD configuration
- class whad.ble.UserDescription(handle: int = 0, description: str = '', characteristic: Characteristic | None = None)[source]
Characteristic user description descriptor.
This descriptor specifies a textual description of the characteristic it is attached to. This description is exposed by the GATT server and can be read by a GATT client.
- __init__(handle: int = 0, description: str = '', characteristic: Characteristic | None = None)[source]
Instantiate a Characteristic User Description descriptor
- Parameters:
description (str) – Set characteristic text description
characteristic (Characteristic, optional) – Characteristic this descriptor is attached to, not required when defining a GATT model.
- property name
Descriptor name
Clean description as it may contain null chars.
- property text: str
User description
- class whad.ble.DeviceInformationService(handle: int = 0, end_handle: int = 0, **children)[source]
Device Information Service version 1.2 as defined in specification.
- class whad.ble.BatteryService(handle: int = 0, end_handle: int = 0)[source]
Battery Service Profile
This service implements the BLE Battery Service as described in the spec.
- property percentage: int
Battery level as percentage.
- class whad.ble.HeartRateService(handle: int = 0, end_handle: int = 0)[source]
Heart Rate service version 1.0 as defined in specification.
- class UpdateEvent(rate: int, energy_expended: int | None, skin_contact: bool | None)[source]
Heart Rate service update event.
- property energy_expended: int | None
Energy expended, if available.
- property heart_rate: int
Heart Rate value
- property skin_contact: bool | None
Skin contact detected, if sensor is present.
- property contact_sensor: bool
Contact sensor present
- property energy_expended: int | None
Energy expended in kJ
- on_update(_: Characteristic, value: bytes, __: bool = False)[source]
Process incoming notifications.
- property rate: int
Heart rate in BPM.
- set_location(location: str)[source]
Update body sensor location’s characteristic value.
- Parameters:
location (str) – Sensor location
- property skin_contact: bool
Skin contact status
Deprecated
- class whad.ble.GenericProfile(start_handle: int = 1, from_json: str | None = None)[source]
Old name of the Profile class, kept for backward compatibility.
Changed in version 1.3.0:
GenericProfilehas been renamed toProfileto simplify code and due to a change in the way standard services are now declared within a GATT profile class.In previous versions, including a Battery Service into a custom profile required to inherit from both
GenericProfileandBatteryService. A generic profile was then considered as an empty GATT profile that could be used to create default profiles, an idea now put aside because it does not fit in our vision of GATT profiles anymore.- __init__(start_handle: int = 1, from_json: str | None = None)
Parse the device model, instantiate all the services, characteristics and descriptors, compute all handle values and registers everything inside this instance for further use.
- Parameters:
start_handle (int) – Start handle value to use (default: 0)
from_json (str) – JSON data describing a GATT profile