Raspberry Pi Pico W: Bluetooth Low Energy (BLE) с MicroPython

Raspberry Pi Pico W (и 2 W) поддерживает Bluetooth Low Energy, что может быть полезно в различных проектах IoT и автоматизации. Эта статья представляет собой руководство по началу работы с Bluetooth Low Energy на Raspberry Pi Pico. Мы рассмотрим, как настроить Raspberry Pi Pico в качестве BLE-периферийного устройства и в качестве центрального BLE-устройства.

Raspberry Pi Pico W: Начало работы с Bluetooth Low Energy BLE MicroPython

Предварительные требования – Прошивка MicroPython

Для выполнения этого руководства вам необходима прошивка MicroPython, установленная на вашу плату Raspberry Pi Pico. Вам также нужна IDE для написания и загрузки кода на плату.

Рекомендуемая IDE для MicroPython на Raspberry Pi Pico – это Thonny IDE. Следуйте следующему руководству, чтобы узнать, как установить Thonny IDE, прошить MicroPython и загрузить код на плату.

Bluetooth на Raspberry Pi Pico W

Raspberry Pi Pico W и Raspberry Pi Pico 2 W оснащены чипом Infineon CYW43439, который добавляет поддержку Wi-Fi и Bluetooth.

Это руководство совместимо только с версиями W Raspberry Pi Pico:

В документации упоминается, что поддерживаются как Bluetooth Classic, так и Bluetooth Low Energy. Однако на данный момент мне не удалось найти примеры для Bluetooth Classic с MicroPython. Поэтому наши примеры будут сосредоточены на Bluetooth Low Energy.

Raspberry Pi Pico 2 W

Поддержка MicroPython для Bluetooth на Pico W появилась относительно недавно, поэтому пока ещё мало поддержки в плане примеров, библиотек, документации и функциональности. Кроме того, некоторые библиотеки всё ещё находятся в бета-версии, и либо у них не все методы реализованы, либо нет примеров и документации, либо есть ошибки или проблемы. Поэтому на данный момент учитывайте это при работе с Bluetooth на Raspberry Pi Pico. Тем не менее, мы предоставим вам основную информацию и базовые рабочие примеры, которые мы смогли запустить и протестировать и которые помогут вам начать работу.

Что такое Bluetooth Low Energy?

Bluetooth Low Energy

Bluetooth Low Energy, сокращённо BLE (также называемый Bluetooth Smart), – это энергосберегающий вариант Bluetooth. Основное применение BLE – это передача небольших объёмов данных на короткие расстояния (низкая пропускная способность). В отличие от Bluetooth, который всегда включён, BLE постоянно находится в спящем режиме, кроме моментов, когда инициируется соединение. Это позволяет ему потреблять очень мало энергии. BLE потребляет примерно в 100 раз меньше энергии, чем Bluetooth (в зависимости от сценария использования).

Основные концепции Bluetooth Low Energy

Прежде чем продолжить, важно ознакомиться с некоторыми базовыми концепциями BLE.

BLE-периферийное устройство и контроллер (центральное устройство)

При использовании Bluetooth Low Energy (BLE) важно понимать роли BLE-периферийного устройства и BLE-контроллера (также называемого центральным устройством).

BLE-периферийное устройство и контроллер -- пример с RPi Pico в качестве BLE-периферийного устройства

Pico W может выступать как в роли периферийного, так и в роли центрального устройства. Когда он действует как периферийное устройство, он настраивает профиль GATT и анонсирует свой сервис с характеристиками, которые центральные устройства могут читать. С другой стороны, когда он настроен как центральное устройство, он может подключаться к другим BLE-устройствам для чтения или взаимодействия с их профилями и чтения их характеристик.

На диаграмме выше Pico выполняет роль BLE-периферийного устройства, выступая в качестве устройства, предоставляющего данные или сервисы. Ваш смартфон или компьютер действует как BLE-контроллер, управляя соединением и связью с Pico.

BLE-сервер и клиент

При использовании Bluetooth Low Energy существуют два типа устройств: сервер и клиент. Pico может выступать как в роли клиента, так и в роли сервера. На рисунке ниже он действует как сервер, предоставляя свою структуру GATT, содержащую данные. BLE-сервер выступает в качестве поставщика данных или сервисов, а BLE-клиент потребляет или использует эти сервисы.

BLE-сервер и клиент -- RPi Pico W в качестве сервера

Сервер анонсирует своё существование, чтобы его могли обнаружить другие устройства, и содержит данные, которые клиент может читать или с которыми может взаимодействовать. Клиент сканирует ближайшие устройства и, когда находит нужный сервер, устанавливает соединение и может взаимодействовать с этим устройством, читая или записывая в его характеристики.

BLE-сервер – это, по сути, BLE-периферийное устройство до установления соединения. BLE-клиент – это BLE-контроллер до установления соединения. Во многих случаях эти термины используются взаимозаменяемо.

GATT

GATT (Generic Attribute Profile – общий профиль атрибутов) – это фундаментальная концепция в технологии Bluetooth Low Energy (BLE). По сути, он служит схемой того, как BLE-устройства общаются друг с другом. Представьте его как структурированный язык, который два BLE-устройства используют для беспрепятственного обмена информацией.

Структура BLE GATT
  • Профиль: стандартная коллекция сервисов для конкретного сценария использования;

  • Сервис: коллекция связанной информации, например, показания датчиков, уровень заряда батареи, частота сердцебиения и т.д.;

  • Характеристика: именно здесь в иерархии сохраняются фактические данные (значение);

  • Дескриптор: метаданные о данных;

  • Свойства: описывают, как можно взаимодействовать со значением характеристики. Например: чтение, запись, уведомление, широковещание, индикация и т.д.

Давайте более подробно рассмотрим BLE-сервис и характеристики.

BLE-сервис

Верхний уровень иерархии – это профиль, который состоит из одного или нескольких сервисов. Обычно BLE-устройство содержит более одного сервиса, например сервис батареи и сервис частоты сердцебиения.

Каждый сервис содержит как минимум одну характеристику. Существуют предопределённые сервисы для нескольких типов данных, определённые SIG (Bluetooth Special Interest Group), такие как: уровень заряда батареи, кровяное давление, частота сердцебиения, весы, измерение окружающей среды и т.д. Вы можете посмотреть предопределённые сервисы по следующей ссылке:

UUID

UUID – это уникальный цифровой идентификатор, используемый в BLE и GATT для различения и обнаружения сервисов, характеристик и дескрипторов. Это как отдельная метка, которая гарантирует, что каждый компонент Bluetooth-устройства имеет уникальное имя.

Каждый сервис, характеристика и дескриптор имеет UUID (Universally Unique Identifier – универсальный уникальный идентификатор). UUID – это уникальное 128-битное (16 байт) число. Например:

55072829-bc9e-4c53-938a-74a6d4c78776

Существуют сокращённые UUID и UUID по умолчанию для сервисов и характеристик, определённых в SIG (Bluetooth Special Interest Group). Это означает, что если у вас есть BLE-устройство, использующее UUID по умолчанию для своих сервисов и характеристик, вы будете точно знать, как взаимодействовать с этим устройством, чтобы получить или обработать нужную вам информацию.

Вы также можете генерировать свои собственные пользовательские UUID, если не хотите использовать предопределённые значения или если передаваемые вами данные не подходят ни к одной из категорий.

Вы можете сгенерировать пользовательские UUID, используя этот сайт-генератор UUID.

Связь между BLE-устройствами

Вот типичные шаги, описывающие связь между BLE-устройствами.

RPi Pico -- связь между BLE-устройствами
  1. BLE-периферийное устройство (сервер) анонсирует своё существование.

  2. Центральное BLE-устройство (клиент) сканирует BLE-устройства.

  3. Когда центральное устройство находит нужное периферийное устройство, оно подключается к нему.

  4. После подключения оно читает профиль GATT периферийного устройства и ищет нужный сервис (например: environmental sensing).

  5. Если сервис найден, оно может взаимодействовать с характеристиками. Например, считывать значение температуры.

Установка пакета aioble

Для написания кода для работы с Bluetooth на Raspberry Pi Pico мы установим пакет aioble – это рекомендуемая на данный момент библиотека для BLE-коммуникации. Перед тем как перейти к реальным примерам, вам нужно установить её на вашу плату.

1) Подключите плату к компьютеру и подключитесь к ней через Thonny IDE.

2) В Thonny IDE перейдите в Tools > Manage Packages…

3) Найдите aioble и нажмите на опцию aioble.

Установка пакета aioble в Thonny IDE

4) Наконец, нажмите кнопку Install.

Установка пакета aioble в Thonny IDE

5) Подождите несколько секунд, пока идёт установка.

Готово. Теперь вы можете использовать функции aioble для программирования Raspberry Pi Pico.


BLE-периферийное и центральное устройство Raspberry Pi Pico – связь между двумя платами

Для этого примера вам нужны две платы Raspberry Pi Pico W или 2W. Мы настроим одну плату как BLE-периферийное устройство, а другую – как центральное BLE-устройство. Вы узнаете, как периферийное устройство анонсирует данные и как центральное устройство может считывать данные с периферийного устройства.

BLE-сервер и клиент -- RPi Pico W в качестве сервера

У вас только одна плата Raspberry Pi Pico W?

Если у вас есть только одна плата Raspberry Pi Pico W, вы можете протестировать и выполнить только пример с периферийным устройством, но вам придётся пропустить центральное устройство.

BLE-периферийное устройство

В этом примере мы создадим BLE-периферийное устройство с Environmental Sensing Service (сервис, определённый SIG) с одной характеристикой для температуры.

  • Сервис: Environmental Sensing Service

    • Характеристика: Temperature

Мы будем использовать UUID по умолчанию для Environmental Sensing Service и характеристики Temperature.

Поиск UUID по умолчанию

Если вы перейдёте на эту страницу и откроете документ Assigned Numbers (PDF), вы найдёте все назначенные по умолчанию UUID. Если вы поищете Environmental Sensing Service, вы найдёте все допустимые характеристики, которые можно использовать с этим сервисом. Вы можете увидеть, что он поддерживает температуру.

Характеристики, поддерживаемые сервисом Environmental Sensing

Есть таблица с UUID для всех сервисов. Вы можете видеть, что UUID для сервиса Environmental Sensing – 0x181A.

UUID сервиса BLE Environmental Sensing

Затем найдите UUID характеристики температуры. Вы найдёте таблицу со значениями для всех характеристик. UUID для температуры – 0x2A6E.

UUID температуры BLE

Итого UUID:

  • Environmental Sensing Service: 0x181A

    • Temperature Characteristic: 0x2A6E

Код – Raspberry Pi Pico W как BLE-периферийное устройство

Следующий код настраивает Raspberry Pi Pico W или 2 W как BLE-периферийное устройство с Environmental Sensing Service и характеристикой Temperature. BLE-устройство будет непрерывно записывать в характеристику Temperature, обновляя её последним значением температуры, и анонсировать свой сервис и характеристику. Мы получаем температуру от встроенного датчика температуры Raspberry Pi Pico (вам нужен установленный пакет picozero).

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/raspberry-pi-pico-w-bluetooth-low-energy-micropython/

from micropython import const
import asyncio
import aioble
import bluetooth
import struct
from picozero import pico_temp_sensor

#org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
# org.bluetooth.characteristic.gap.appearance.xml
_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768)
# How frequently to send advertising beacons.
_ADV_INTERVAL_MS = 250_000

# Register GATT server.
temp_service = aioble.Service(_ENV_SENSE_UUID)
temp_characteristic = aioble.Characteristic(
temp_service, _ENV_SENSE_TEMP_UUID, read=True, notify=True)
aioble.register_services(temp_service)

# Helper to encode the temperature characteristic encoding
# (sint16, hundredths of a degree).
def _encode_temperature(temp_deg_c):
    return struct.pack("<h", int(temp_deg_c * 100))

# Get temperature and update characteristic
async def sensor_task():
    while True:
        temperature = pico_temp_sensor.temp
        temp_characteristic.write(_encode_temperature(temperature), send_update=True)
        print(temperature)
        await asyncio.sleep_ms(1000)

# Serially wait for connections. Don't advertise while a central is connected.
async def peripheral_task():
    while True:
        try:
            async with await aioble.advertise(
                _ADV_INTERVAL_MS,
                name="RPi-Pico",
                services=[_ENV_SENSE_UUID],
                appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER,
                ) as connection:
                    print("Connection from", connection.device)
                    await connection.disconnected()
        except asyncio.CancelledError:
            # Catch the CancelledError
            print("Peripheral task cancelled")
        except Exception as e:
            print("Error in peripheral_task:", e)
        finally:
            # Ensure the loop continues to the next iteration
            await asyncio.sleep_ms(100)

# Run both tasks
async def main():
    t1 = asyncio.create_task(sensor_task())
    t2 = asyncio.create_task(peripheral_task())
    await asyncio.gather(t1, t2)

asyncio.run(main())

Просмотреть исходный код

Этот пример основан на примере aioble, который можно найти здесь.

Как работает код

Давайте быстро рассмотрим основные части кода этого примера.

Подключение библиотек

Вам нужно подключить библиотеки aioble и bluetooth для использования Bluetooth с Pico.

import aioble
import bluetooth

Наш код будет асинхронным. Для этого мы используем библиотеку asyncio. Мы рекомендуем пройти это руководство для знакомства с асинхронным программированием: Raspberry Pi Pico Asynchronous Programming – Run Multiple Tasks (MicroPython).

import asyncio

Определение UUID и регистрация GATT-сервиса и характеристики

Мы определяем UUID для сервиса Environmental Sensing и для характеристики температуры.

# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)

# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)

Затем регистрируем GATT-сервис и характеристику.

# Register GATT server.
temp_service = aioble.Service(_ENV_SENSE_UUID)
temp_characteristic = aioble.Characteristic(
    temp_service, _ENV_SENSE_TEMP_UUID, read=True, notify=True
)
aioble.register_services(temp_service)

При настройке характеристики мы устанавливаем аргументы read и notify в True. Это определяет способ, которым центральное устройство может взаимодействовать с характеристикой. Оно может читать характеристику и получать уведомления при её изменении.

Для записи фактического значения температуры в характеристику температуры оно должно быть в определённом формате. Функция _encode_temperature() выполняет это преобразование.

def _encode_temperature(temp_deg_c):
    return struct.pack("<h", int(temp_deg_c * 100))

Получение температуры и запись в характеристику

У нас есть асинхронная функция, которая получает температуру от внутреннего датчика температуры и записывает в характеристику температуры с помощью метода write() объекта temp_characteristic. Эта задача повторяется непрерывно каждую секунду. Вы можете настроить время задержки по мере необходимости.

# Get temperature and update characteristic
async def sensor_task():
    while True:
        temperature = pico_temp_sensor.temp
        temp_characteristic.write(_encode_temperature(temperature), send_update=True)
        print(temperature)
        await asyncio.sleep_ms(1000)

Анонсирование (Advertising)

Помимо записи в характеристику температуры, нам также нужно анонсировать Raspberry Pi Pico как BLE-сервис. Для этого мы используем функцию peripheral_task().

# Serially wait for connections. Don't advertise while a central is connected.
async def peripheral_task():
    while True:
        try:
            async with await aioble.advertise(
                _ADV_INTERVAL_MS,
                name="RPi-Pico",
                services=[_ENV_SENSE_UUID],
                appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER,
            ) as connection:
                print("Connection from", connection.device)
                await connection.disconnected()
        except asyncio.CancelledError:
            # Catch the CancelledError
            print("Peripheral task cancelled")
        except Exception as e:
            print("Error in peripheral_task:", e)
        finally:
            # Ensure the loop continues to the next iteration
            await asyncio.sleep_ms(100)

В этой функции мы определяем имя BLE-устройства.

name="RPi-Pico",

Вы можете изменить его имя, если хотите. В следующем примере мы будем подключаться к этому устройству по его имени, поэтому вам нужно убедиться, что вы используете одинаковое имя в обоих кодах, как мы увидим позже.

Функция main

Наконец, мы создаём асинхронную функцию main(), где напишем основу нашего кода. Мы создаём две асинхронные задачи: одну для анонсирования и другую для записи в характеристику температуры.

# Run both tasks.
async def main():
    t1 = asyncio.create_task(sensor_task())
    t2 = asyncio.create_task(peripheral_task())

    await asyncio.gather(t1, t2)

asyncio.run(main())

Тестирование кода

Запустите предыдущий код на вашем Raspberry Pi Pico. Он начнёт записывать температуру в характеристику температуры и анонсировать свой сервис.

RPi Pico BLE -- начало анонсирования

Чтобы подключиться к этому периферийному устройству и прочитать его характеристику, мы создадим центральное BLE-устройство на другой плате Raspberry Pi Pico.

Приложение nRF Connect

Прежде чем продолжить, вы можете использовать приложение nRF Connect для поиска BLE-устройств и чтения их характеристик.

Приложение nRF Connect от Nordic работает на Android (Google Play Store) и iOS (App Store). Перейдите в Google Play Store или App Store, найдите «nRF Connect for Mobile» и установите приложение на свой смартфон.

Приложение nRF Connect for Mobile

Перейдите на смартфон, откройте приложение nRF Connect от Nordic и начните сканирование новых устройств. Вы должны найти устройство с именем RPi-Pico – это имя BLE-сервера, которое вы определили ранее.

Сканирование RPi Pico -- BLE-устройство в приложении NRF Connect

Подключитесь к нему. Вы увидите, что отображается сервис Environmental Sensing с характеристикой temperature. Нажмите на стрелки, чтобы прочитать характеристику и активировать уведомления.

RPi Pico как BLE-устройство -- чтение характеристики Temperature в приложении nRF Connect

Затем нажмите на второй значок слева, чтобы изменить формат (эта опция может быть доступна только для iPhone, и на нём температура будет автоматически отображаться в правильном формате).

Приложение nRF Connect -- значок изменения формата

Вы можете изменить формат на Unsigned Int.

nRF Connect -- изменение данных на unsigned int

Вы начнёте видеть значения температуры, которые сообщаются каждые 10 секунд. Температура будет отображаться в градусах Цельсия, умноженных на 100 (на iPhone).

RPi Pico как BLE-устройство -- чтение температуры из приложения nRF Connect

Теперь, когда мы знаем, что наше BLE-периферийное устройство работает как ожидалось, мы можем запрограммировать другую плату Raspberry Pi Pico как центральное BLE-устройство для чтения характеристики температуры с этой платы.

Код – Raspberry Pi Pico как центральное BLE-устройство (BLE-клиент)

Следующий код должен быть запущен на другой плате Raspberry Pi Pico. Мы настроим его как центральное BLE-устройство, которое будет искать устройство RPi-Pico, искать сервис Environmental Sensing и считывать температуру из характеристики.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/raspberry-pi-pico-w-bluetooth-low-energy-micropython/

from micropython import const
import uasyncio as asyncio
import aioble
import bluetooth
import struct

# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)

# Name of the peripheral you want to connect
peripheral_name="RPi-Pico"

# Helper to decode the temperature characteristic encoding (sint16, hundredths of a degree).
def _decode_temperature(data):
    try:
        if data is not None:
            return struct.unpack("<h", data)[0] / 100
    except Exception as e:
        print("Error decoding temperature:", e)
    return None

async def find_temp_sensor():
    # Scan for 5 seconds, in active mode, with a very low interval/window (to
    # maximize detection rate).
    async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner:
        async for result in scanner:
            print(result.name())
            # See if it matches our name and the environmental sensing service.
            if result.name() == peripheral_name and _ENV_SENSE_UUID in result.services():
                return result.device
    return None

async def main():
    while True:
        device = await find_temp_sensor()
        if not device:
            print("Temperature sensor not found. Retrying...")
            await asyncio.sleep_ms(5000)  # Wait for 5 seconds before retrying
            continue

        try:
            print("Connecting to", device)
            connection = await device.connect()
        except asyncio.TimeoutError:
            print("Timeout during connection. Retrying...")
            await asyncio.sleep_ms(5000)  # Wait for 5 seconds before retrying
            continue

        async with connection:
            try:
                temp_service = await connection.service(_ENV_SENSE_UUID)
                temp_characteristic = await temp_service.characteristic(_ENV_SENSE_TEMP_UUID)
            except asyncio.TimeoutError:
                print("Timeout discovering services/characteristics. Retrying...")
                await asyncio.sleep_ms(5000)  # Wait for 5 seconds before retrying
                continue

            while True:
                try:
                    temp_data = await temp_characteristic.read()
                    if temp_data is not None:
                        temp_deg_c = _decode_temperature(temp_data)
                        if temp_deg_c is not None:
                            print("Temperature: {:.2f}".format(temp_deg_c))
                        else:
                            print("Invalid temperature data")
                    else:
                        print("Error reading temperature: None")
                except Exception as e:
                    print("Error in main loop:", e)
                    break  # Break out of the inner loop and attempt to reconnect

                await asyncio.sleep_ms(1000)

# Create an Event Loop
loop = asyncio.get_event_loop()
# Create a task to run the main function
loop.create_task(main())

try:
    # Run the event loop indefinitely
    loop.run_forever()
except Exception as e:
    print('Error occurred: ', e)
except KeyboardInterrupt:
    print('Program Interrupted by the user')

Просмотреть исходный код

Этот пример основан на примере aioble, который можно найти здесь.

Как работает код

Давайте быстро рассмотрим основные части кода этого примера.

Подключение библиотек

Вам нужно подключить библиотеки aioble и bluetooth для использования Bluetooth с Pico.

import aioble
import bluetooth

Наш код будет асинхронным. Для этого мы используем библиотеку asyncio. Мы рекомендуем пройти это руководство для знакомства с асинхронным программированием: Raspberry Pi Pico Asynchronous Programming – Run Multiple Tasks (MicroPython).

import asyncio

Определение UUID

Мы определяем UUID для сервиса Environmental Sensing и для характеристики температуры, которую хотим считать.

# org.bluetooth.service.environmental_sensing
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)

# org.bluetooth.characteristic.temperature
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)

Имя BLE-устройства для подключения

Затем мы определяем имя BLE-устройства, к которому хотим подключиться. Это должно быть имя BLE-периферийного устройства, которое вы настроили ранее, в нашем случае – RPi-Pico.

# Name of the peripheral you want to connect
peripheral_name="RPi-Pico"

Температура сохраняется в характеристике в определённом формате. Чтобы помочь нам преобразовать это значение в читаемое, мы используем функцию _decode_temperature().

# Helper to decode the temperature characteristic encoding
# (sint16, hundredths of a degree).
def _decode_temperature(data):
    try:
        if data is not None:
            return struct.unpack("<h", data)[0] / 100
    except Exception as e:
        print("Error decoding temperature:", e)
    return None

Сканирование и поиск BLE-устройств

Функция find_temp_sensor() будет сканировать ближайшие BLE-устройства, и когда найдёт BLE-устройство с определённым нами ранее именем, вернёт это BLE-устройство через result.device.

async def find_temp_sensor():
    # Scan for 5 seconds, in active mode, with a very low interval/window
    # (to maximize detection rate)
    async with aioble.scan(5000, interval_us=30000, window_us=30000, active=True) as scanner:
        async for result in scanner:
            print(result.name())
            # See if it matches our name and the environmental sensing service.
            if result.name() == peripheral_name and _ENV_SENSE_UUID in result.services():
                return result.device
    return None

Функция main

В асинхронной функции main() мы сначала пытаемся найти нужное BLE-устройство, вызывая find_temp_sensor(). Мы сохраняем BLE-устройство в переменной device.

device = await find_temp_sensor()

Затем пытаемся подключиться к этому устройству:

try:
    print("Connecting to", device)
    connection = await device.connect()

После успешного подключения мы получаем сервис температуры в переменную temp_service. Затем используем этот сервис для получения нужной характеристики и сохраняем её в переменной temp_characteristic.

async with connection:
    try:
        temp_service = await connection.service(_ENV_SENSE_UUID)
        temp_characteristic = await temp_service.characteristic(_ENV_SENSE_TEMP_UUID)

Наконец, мы можем получить значение температуры, считав характеристику через temp_characteristic.read().

while True:
    try:
        temp_data = await temp_characteristic.read()
        if temp_data is not None:
            temp_deg_c = _decode_temperature(temp_data)
            if temp_deg_c is not None:
                 print("Temperature: {:.2f}".format(temp_deg_c))
             else:
                 print("Invalid temperature data")
          else:
              print("Error reading temperature: None")
      except Exception as e:
          print("Error in main loop:", e)
          break  # Break out of the inner loop and attempt to reconnect
     await asyncio.sleep_ms(1000)

Мы создаём цикл событий и связываем задачу main() с этим циклом.

# Create an Event Loop
loop = asyncio.get_event_loop()
# Create a task to run the main function
loop.create_task(main())

Наконец, мы запускаем цикл событий, чтобы наш код продолжал работать.

# Run the event loop indefinitely
loop.run_forever()

Тестирование кода

Для тестирования этого кода будет полезно иметь два экземпляра Thonny IDE, чтобы мы могли запускать код на обоих устройствах и видеть консоль для обоих подключений.

Чтобы активировать несколько экземпляров Thonny IDE, перейдите в Tools > Options и снимите галочку с опции Allow only single Thonny instance.

Thonny IDE -- разрешение нескольких экземпляров

Закройте Thonny, чтобы изменения вступили в силу. Затем просто откройте Thonny IDE на вашем компьютере дважды, чтобы открыть два окна.

Убедитесь, что каждое окно подключено к правильной плате. Вы можете выбрать COM-порт в правом нижнем углу.

После того как BLE-периферийное устройство запущено (убедитесь, что оно анонсирует температуру), скопируйте код, предоставленный в этом разделе, в другой экземпляр Thonny IDE (убедитесь, что он подключён к правильному COM-порту). Затем запустите его на этой новой плате.

Он начнёт сканирование BLE-устройств. Когда найдёт устройство RPi-Pico, он подключится к нему и отобразит значения температуры, отправленные другой платой, в Shell.

RPi Pico как BLE-клиент -- чтение BLE-характеристики и отображение в консоли Thonny IDE

Заключение

В этом руководстве вы изучили основы Bluetooth Low Energy с Raspberry Pi Pico при программировании на MicroPython. Вы узнали, как настроить Pico в качестве центрального BLE-устройства и в качестве BLE-периферийного устройства. Вы научились задавать и считывать значения характеристик.

Для дальнейшего развития этого проекта вы можете добавить BME280 или другой датчик по вашему выбору и отображать характеристики температуры, влажности и давления. Вы также можете добавить OLED или LCD к центральному BLE-устройству для отображения показаний с другой платы.

Дополнительные ресурсы по BLE

Для более глубокого изучения Bluetooth вы можете ознакомиться со следующими ресурсами: