Двусторонняя связь по BLE между Raspberry Pi и Raspberry Pi Pico W

Перевод статьи Bidirecional BLE Communication between Raspberry Pi and Raspberry Pi Pico W — Rui Santos & Sara Santos, Random Nerd Tutorials.
Научимся налаживать двустороннюю связь по Bluetooth Low Energy (BLE) между Raspberry Pi и Raspberry Pi Pico. Pico будет настроен как BLE-периферия с двумя характеристиками. Raspberry Pi будет читать и писать в эти характеристики — так и получится двусторонняя связь.
Для программы на Raspberry Pi (Python) используем библиотеку bleak, а для программы на Pico (MicroPython) — библиотеку aioble.
Это руководство основано на коде и наработках Edgardo Peregrino.
Если тема BLE для вас новая, рекомендуем сначала прочитать вводные материалы:
- BLE Communication between Raspberry Pi and Raspberry Pi Pico W
- Raspberry Pi Pico W: Bluetooth Low Energy (BLE) with MicroPython
Что понадобится заранее#
- Raspberry Pi с поддержкой Bluetooth (например, RPi 4 или RPi 5).
- Pico W или Pico 2 W.
- На Pi: создать виртуальное окружение, установить в него библиотеку Bleak (и pigpio) — как описано в разделе «Preparing the Raspberry Pi» вводного руководства.
- Прошить на Pico W или Pico 2 W актуальную версию прошивки MicroPython.
Что нужно из железа#
- Raspberry Pi (мы используем Raspberry Pi 5);
- Raspberry Pi Pico W или Pico 2 W;
- 1× светодиод;
- 1× резистор 220 Ом (или близкого номинала);
- макетная плата;
- соединительные провода.
Схема подключения#
Схема та же, что и во вводном руководстве. Для Raspberry Pi нужен светодиод, подключённый к GPIO 17, — он даст визуальную обратную связь и покажет, что код работает. Pico ставится на макетную плату просто для удобства.

Светодиод на GPIO 17 у Raspberry Pi и Pico W на макетной плате.
Как работает двусторонняя связь?#
Raspberry Pi Pico настраивает GATT-сервис с двумя характеристиками:
- rx_characteristic — характеристика, в которую пишет Pi. Pico читает её значение (получает данные от Pi).
- tx_characteristic — характеристика, в которую пишет Pico. Pico отправляет данные в Pi.

Двусторонняя связь через характеристики rx и tx.
Код: Raspberry Pi Pico W (MicroPython)#
Начнём с кода для Pico. Скопируйте его в Thonny IDE. Понадобится установить пакет aioble: в Thonny откройте Tools → Manage Packages…, найдите aioble и нажмите Install.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/bidirecional-ble-communication-raspberry-pi-and-pico/
import asyncio
import aioble
import bluetooth
# Bluetooth configuration
_SERVICE_UUID = bluetooth.UUID(0x1848)
# Characteristic where the Raspberry Pi writes data TO the Pico (Inbox)
_PI_TO_PICO_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6E)
# Characteristic where the Pico writes data TO the Raspberry Pi (Outbox)
_PICO_TO_PI_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6F)
BLE_NAME = "Pico W Peripheral"
# Register GATT server
ble_service = aioble.Service(_SERVICE_UUID)
# Inbox: Raspberry Pi writes data to the Pico
rx_characteristic = aioble.Characteristic(
ble_service,
_PI_TO_PICO_CHARACTERISTIC_UUID,
read=True,
write=True,
capture=True
)
# Outbox: Pico writes data to the Raspberry Pi
tx_characteristic = aioble.Characteristic(
ble_service,
_PICO_TO_PI_CHARACTERISTIC_UUID,
read=True,
write=True,
notify=True,
capture=True
)
aioble.register_services(ble_service)
# Helper functions
def _encode_message(message):
"""Encode a message to bytes."""
return message.encode('utf-8')
def _decode_message(data):
"""Decode a message from bytes."""
return data.decode('utf-8')
# Task to handle sending data
async def send_task():
message_count = 1
while True:
message = f" From {BLE_NAME}!: {message_count}"
message_count += 1
try:
tx_characteristic.write(_encode_message(message), send_update=True)
print(f"Sent: {message}")
except Exception as e:
print(f"Error while sending data: {e}")
await asyncio.sleep_ms(1000)
# Task to handle receiving data
async def receive_task():
last_data = None
while True:
try:
data = rx_characteristic.read()
if data and data != last_data:
decoded = _decode_message(data)
print(f"Received: {decoded}")
last_data = data
await asyncio.sleep_ms(500)
except Exception as e:
print(f"Error receiving data: {e}")
await asyncio.sleep_ms(500)
# Serially wait for connections
async def peripheral_task():
# Show MAC address once at start
ble = bluetooth.BLE()
_, mac_address = ble.config('mac')
formatted_mac = ':'.join('{:02X}'.format(b) for b in mac_address)
print(f"Bluetooth MAC Address: {formatted_mac}")
while True:
try:
async with await aioble.advertise(
2000, name=BLE_NAME, services=[_SERVICE_UUID], appearance=768
) as connection:
print("Connection from", connection.device)
await connection.disconnected()
except Exception as e:
print("Error in peripheral_task:", e)
finally:
print(f"{BLE_NAME} disconnected")
await asyncio.sleep_ms(100)
# Run all tasks
async def main():
t1 = asyncio.create_task(send_task())
t2 = asyncio.create_task(receive_task())
t3 = asyncio.create_task(peripheral_task())
await asyncio.gather(t1, t2, t3)
# Run the program
asyncio.run(main())
Мы используем библиотеку aioble, чтобы настроить Pico как BLE-периферию.
Как работает код#
Сначала импортируем библиотеки.
import asyncio
import aioble
import bluetooth
Задаём UUID для GATT-сервиса, для характеристики, куда будем писать данные, и для характеристики, откуда будем их читать. Также задаём имя устройства.
_SERVICE_UUID = bluetooth.UUID(0x1848)
_PI_TO_PICO_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6E)
_PICO_TO_PI_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6F)
BLE_NAME = "Pico W Peripheral"
Создаём сервис GATT-сервера по заданному UUID — он служит «контейнером» для характеристик.
ble_service = aioble.Service(_SERVICE_UUID)
Первая характеристика — та, в которую Raspberry Pi может писать (write=True); Pico может читать записанные данные (read=True), а capture=True заставляет Pico автоматически хранить последнее значение.
Вторая характеристика служит для отправки данных от Pico к Pi; notify=True позволяет Pico автоматически уведомлять Pi при появлении новых данных.
Функции _encode_message() и _decode_message() кодируют и декодируют сообщение в UTF-8.
Отправка данных. Асинхронная задача send_task() каждую секунду пишет сообщение со счётчиком в tx_characteristic.
tx_characteristic.write(_encode_message(message), send_update=True)
Приём данных. Задача receive_task() каждые 500 мс читает значение rx_characteristic и печатает его в Shell, только если оно изменилось.
data = rx_characteristic.read()
Pico как BLE-периферия. Задача peripheral_task() сначала печатает Bluetooth MAC-адрес, а затем начинает «рекламировать» (advertise) Pico как периферию с заданным ранее UUID сервиса.
Запуск задач. В main() создаём три асинхронные задачи (отправка, приём, периферия) и запускаем их через asyncio.gather().
Запуск кода на Pico#
Запустите код на Pico зелёной кнопкой Play в Thonny (или клавишей F5). После запуска отобразится Bluetooth MAC-адрес Pico — сохраните его, он понадобится в коде для Raspberry Pi.

MAC-адрес Pico в Thonny IDE.
Код: Raspberry Pi (Python)#
На Raspberry Pi создайте файл с кодом ниже, например pi_send_receive.py. Он должен лежать в той же папке, где вы создавали виртуальное окружение.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/bidirecional-ble-communication-raspberry-pi-and-pico/
import asyncio
from bleak import BleakClient, uuids
from gpiozero import LED
connected = False
led = LED(17)
# REPLACE WITH YOUR PICO MAC ADDRESS
pico_address = "FF:FF:FF:FF:FF:FF"
SERVICE_UUID = uuids.normalize_uuid_16(0x1848)
WRITE_CHARACTERISTIC_UUID = uuids.normalize_uuid_16(0x2A6E)
READ_CHARACTERISTIC_UUID = uuids.normalize_uuid_16(0x2A6F)
message_count = 0
async def send_data_task(client):
global message_count
while True:
message_count += 1
message = f"From central: {message_count}".encode("utf-8")
try:
if not client.is_connected:
break
await client.write_gatt_char(WRITE_CHARACTERISTIC_UUID, message)
print(f"Central sent: {message.decode('utf-8')}")
except Exception as e:
print(f"Send error: {e}")
break
await asyncio.sleep(5)
async def receive_data_task(client):
last_message = None
while True:
try:
if not client.is_connected:
break
response = await client.read_gatt_char(READ_CHARACTERISTIC_UUID)
if response:
message = response.decode("utf-8")
if message != last_message:
print(f"Central received: {message}")
last_message = message
except Exception as e:
print(f"Receive error: {e}")
break
await asyncio.sleep(2)
async def blink_task():
global connected
print("blink task started")
while True:
led.toggle()
blink = 1000 if connected else 250
await asyncio.sleep(blink / 1000)
async def connect_and_communicate(address):
global connected
print(f"Connecting to {address}...")
blink_task_obj = asyncio.create_task(blink_task())
while True:
client = None
try:
client = BleakClient(address)
await client.connect()
connected = True
print("Connected to Pico")
tasks = [
asyncio.create_task(send_data_task(client)),
asyncio.create_task(receive_data_task(client))
]
done, pending = await asyncio.wait(tasks, timeout=30, return_when=asyncio.FIRST_COMPLETED)
for t in pending:
t.cancel()
except asyncio.CancelledError:
raise
except Exception as e:
print(f"Connection failed or lost: {e}")
finally:
connected = False
if client:
try:
if client.is_connected:
await asyncio.wait_for(client.disconnect(), timeout=2.0)
except:
pass
print("Disconnected - trying to reconnect in 3 seconds...")
await asyncio.sleep(3)
async def main():
try:
await connect_and_communicate(pico_address)
except asyncio.CancelledError:
print("\nCancelling tasks...")
except KeyboardInterrupt:
print("\nProgram stopped by user")
except Exception as e:
print(f"Unexpected error: {e}")
finally:
connected = False
led.off()
print("Cleanup completed.")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
led.off()
Перед запуском впишите в эту строку Bluetooth MAC-адрес вашего Pico (вы получили его в предыдущем примере):
pico_address = "2C:CF:67:B6:D7:4C"
Как работает код#
Raspberry Pi подключается к Pico как BLE-клиент: пишет в характеристику, которую читает Pico, и читает характеристику, в которую пишет Pico.
Импортируем библиотеки — среди них bleak для Bluetooth.
import asyncio
from bleak import BleakClient, uuids
from gpiozero import LED
Булева переменная connected отслеживает наличие соединения, а GPIO 17 задаётся объектом LED для визуальной обратной связи.
UUID сервиса и характеристик должны совпадать с теми, что заданы на Pico.
SERVICE_UUID = uuids.normalize_uuid_16(0x1848)
WRITE_CHARACTERISTIC_UUID = uuids.normalize_uuid_16(0x2A6E)
READ_CHARACTERISTIC_UUID = uuids.normalize_uuid_16(0x2A6F)
Запись в характеристику (отправка). Функция send_data_task() каждые пять секунд пишет в rx_characteristic Pico.
await client.write_gatt_char(WRITE_CHARACTERISTIC_UUID, message)
Чтение характеристики (приём). Функция receive_data_task() каждые две секунды читает значение характеристики периферии.
response = await client.read_gatt_char(READ_CHARACTERISTIC_UUID)
Мигание светодиодом. Задача blink_task() работает параллельно: мигает светодиодом на GPIO 17 раз в секунду при наличии соединения и каждые 250 мс, если соединения нет.
Подключение к BLE-устройству. Функция connect_and_communicate() пытается подключиться к периферии с заданным MAC-адресом. Сначала запускается blink_task() для мгновенной обратной связи, затем в бесконечном цикле создаётся BLE-клиент и устанавливается соединение. После подключения запускаются задачи отправки и приёма; программа ждёт, пока одна из них завершится или пройдёт 30 секунд, после чего отменяет оставшуюся и готовится к переподключению. При ошибке соединение аккуратно закрывается, и через паузу — новая попытка.
Запуск Python-кода#
Создав файл pi_send_receive.py с вашим MAC-адресом Pico, запустите его на Raspberry Pi. Сначала убедитесь, что вы в созданном ранее виртуальном окружении; если вышли из него — активируйте снова:
source blue/bin/activate
Запуск программы:
python3 pi_send_receive.py
Демонстрация#
Код запустится и через несколько секунд подключится к Pico (убедитесь, что на Pico запущен код в Thonny). Raspberry Pi начнёт «отправлять» и «получать» данные, а светодиод на GPIO 17 — мигать раз в секунду.

Отправка и приём BLE-сообщений на стороне Raspberry Pi.
В окне Thonny видно, как Pico «получает» и «отправляет» данные Pi.

Отправка и приём сообщений на стороне Pico.

Двусторонняя связь между Raspberry Pi и Pico W в работе.
Готово — вы наладили двустороннюю связь между платами Raspberry Pi и Pico.
Итог#
В этом руководстве вы научились налаживать двустороннюю связь по BLE между Raspberry Pi Pico и Raspberry Pi. Pico настраивает GATT-сервис с двумя характеристиками, а запись и чтение этих характеристик позволяют платам обмениваться данными. Мы просто шлём сообщение со счётчиком, но это легко переделать под передачу показаний датчиков, состояния GPIO и не только.
Отдельная благодарность читателю Edgardo Peregrino, который создал основу этого руководства и примеры кода.
Спасибо за чтение.