Minimal Python script to list & read BLE device characteristics using Python (Bleak)

This script lists Bluetooth Low Energy (BLE) devices and reads their characteristics using the Bleak library in Python. It is a minimal example to demonstrate how to connect to a BLE device and read its characteristics.

#!/usr/bin/env python3
"""
BLE Device Connection and Service Explorer

This script connects to a specific BLE device by MAC address and lists all
available services and their characteristics/attributes.

Requirements:
    - bleak library: pip install bleak

Usage:
    python connect_ble_device.py [MAC_ADDRESS]
    
Example:
    python connect_ble_device.py 24:EC:4A:76:00:32
"""

import asyncio
import sys
import argparse
from bleak import BleakClient
from bleak.exc import BleakError
from datetime import datetime

async def explore_device_services(client, show_descriptors=False):
    """
    Explore all services and characteristics of a connected BLE device.
    
    Args:
        client (BleakClient): Connected BLE client
    """
    try:
        # Get all services as a list
        services = list(client.services)
        if not services:
            print("No services found on this device.")
            return

        print(f"Found {len(services)} service(s):")
        print("=" * 80)

        for service in services:
            print(f"\nService: {service.uuid}")
            print(f"Description: {service.description}")
            print(f"Handle: {service.handle}")

            # Get characteristics for this service
            characteristics = service.characteristics

            if characteristics:
                print(f"  Characteristics ({len(characteristics)}):")
                print("  " + "-" * 76)

                for char in characteristics:
                    print(f"    UUID: {char.uuid}")
                    print(f"    Description: {char.description}")
                    print(f"    Handle: {char.handle}")
                    print(f"    Properties: {', '.join(char.properties)}")

                    # Try to read the characteristic if it's readable
                    if "read" in char.properties:
                        try:
                            value = await client.read_gatt_char(char.uuid)
                            # Try to decode as string, otherwise show as hex
                            try:
                                decoded_value = value.decode('utf-8')
                                print(f"    Value (string): {decoded_value}")
                            except UnicodeDecodeError:
                                hex_value = ' '.join(f'{b:02x}' for b in value)
                                print(f"    Value (hex): {hex_value}")
                                print(f"    Value (raw bytes): {value}")
                        except Exception as e:
                            print(f"    Value: <Could not read - {e}>")

                    # Show descriptors only if requested
                    if show_descriptors:
                        descriptors = char.descriptors
                        if descriptors:
                            print(f"    Descriptors ({len(descriptors)}):")
                            for desc in descriptors:
                                print(f"      UUID: {desc.uuid}")
                                print(f"      Description: {desc.description}")
                                print(f"      Handle: {desc.handle}")
                                # Try to read descriptor if possible
                                try:
                                    desc_value = await client.read_gatt_descriptor(desc.handle)
                                    try:
                                        decoded_desc = desc_value.decode('utf-8')
                                        print(f"      Value (string): {decoded_desc}")
                                    except UnicodeDecodeError:
                                        hex_desc = ' '.join(f'{b:02x}' for b in desc_value)
                                        print(f"      Value (hex): {hex_desc}")
                                except Exception as e:
                                    print(f"      Value: <Could not read - {e}>")
                            print()  # Empty line between characteristics
                    else:
                        print()
            else:
                print("    No characteristics found for this service.")

            print("-" * 80)
    
    except Exception as e:
        print(f"Error exploring services: {e}")


async def connect_and_explore(mac_address, show_descriptors=False):
    """
    Connect to a BLE device and explore its services.
    
    Args:
        mac_address (str): MAC address of the device to connect to
        scan_time (int): Duration to scan for the device
    """
    print(f"\nAttempting to connect to {mac_address} ...")
    try:
        async with BleakClient(mac_address) as client:
            if client.is_connected:
                print(f"Successfully connected to {mac_address}")
                print(f"Connected at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
                print()
                # Explore all services and characteristics
                await explore_device_services(client, show_descriptors=show_descriptors)
                print(f"\nDisconnected from {mac_address}")
                return True
            else:
                print(f"Failed to connect to {mac_address}")
                return False
    except BleakError as e:
        print(f"Bluetooth error: {e}")
        return False
    except Exception as e:
        print(f"Unexpected error: {e}")
        return False


async def main():
    """
    Main function to run the BLE device connector and explorer.
    """
    print("BLE Device Connection and Service Explorer")
    print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print()

    parser = argparse.ArgumentParser(description="Connect to a BLE device and list all services and attributes.")
    parser.add_argument("mac_address", nargs="?", default="24:EC:4A:76:00:32", help="MAC address of the BLE device (default: 24:EC:4A:76:00:32)")
    parser.add_argument("-d", "--descriptors", action="store_true", help="Show individual descriptors for each characteristic")
    args = parser.parse_args()

    mac_address = args.mac_address
    show_descriptors = args.descriptors

    print(f"Using MAC address: {mac_address}")
    if show_descriptors:
        print("Descriptor display enabled.")

    # Validate MAC address format (basic check)
    if len(mac_address.replace(":", "").replace("-", "")) != 12:
        print(f"Invalid MAC address format: {mac_address}")
        print("Expected format: XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX")
        return

    print()

    # Connect and explore the device
    success = await connect_and_explore(mac_address, show_descriptors=show_descriptors)

    if success:
        print("\nDevice exploration completed successfully.")
    else:
        print("\nDevice exploration failed.")


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nOperation interrupted by user.")
    except Exception as e:
        print(f"An error occurred: {e}")
        sys.exit(1)