This tutorial explains how to set up Bluetooth Low Energy (BLE) communication between a Raspberry Pi and a Pico W. We’ll start by covering the basics of BLE, including the roles of Central and Peripheral devices. After that, we’ll go through two simple examples where the Pico sends data to the Pi.

New to the Raspberry Pi Pico? Learn Raspberry Pi Pico/Pico W with MicroPython with our eBook
This tutorial was written by Edgardo Peregrino and edited by Sara Santos.
Prerequisites
Here’s a list of prerequisites for this tutorial.
1) Raspberry Pi Board
You need a Raspberry Pi board that has Bluetooth. The following models can be used: Pi 3, 3B+, 3A+, 4, 400, 5, 500, Zero W, Zero 2W, CM4 and CM5. When using the CM4 and CM5 modules, ensure that they include the WiFi/Bluetooth module.

1.1) Raspberry Pi OS
In terms of software, you can use either Raspberry Pi OS Desktop or Raspberry Pi OS Lite. We use the 64-bit version.
- To learn how to install the operating system, you can check this tutorial: Install Raspberry Pi OS, Set Up Wi-Fi, Enable and Connect with SSH.
1.2) Terminal and SSH
You should also have some understanding of the terminal, especially since you’ll need to enable the Bluetooth service using the terminal and install essential packages.
We’ll use PuTTY software to establish an SSH connection with the RPi board to run the commands in the terminal window.
We recommend following the next tutorial if you’re not familiar with installing the Raspberry Pi OS, or establishing a connection via SSH using PuTTY:
2) Raspberry Pi Pico Board
You need a Raspberry Pi Pico board that comes with Bluetooth. You can use a Raspberry Pi Pico W or Raspberry Pi Pico 2 W.
2.1) Thonny IDE
To program the Raspberry Pi Pico, we like to use Thonny IDE. Make sure your board is running the latest version of MicroPython firmware. We recommend taking a quick look at the following guide to check how to use Thonny IDE, how to flash MicroPython firmware, and how to run and upload code to the board:
- Getting Started with Raspberry Pi Pico (and Pico W)
- Getting Started with Raspberry Pi Pico 2 and Pico 2 W
The Basics of BLE or Bluetooth Low Energy
Before we begin, we should discuss the basics of BLE or Bluetooth Low Energy. BLE is a protocol that is different from Bluetooth Classic.

Bluetooth Classic is designed for continuous data streaming, such as playing music on a wireless speaker. In contrast, Bluetooth Low Energy (BLE) is optimized for devices and sensors, making it suitable for the Pico W and Raspberry Pi models with built-in Bluetooth.
BLE consumes significantly less power than Bluetooth Classic, making it ideal for microcontrollers and small devices, although it has a shorter range. Another key difference is how they connect: Bluetooth Classic uses Serial communication, while BLE relies on the GATT (Generic Attribute Profile) protocol.
For a more comprehensive introduction to BLE and basic concepts such as peripheral and controller, UUIDs, GATT profiles, and more, we recommend reading the introduction section of this tutorial: Raspberry Pi Pico W: Bluetooth Low Energy (BLE) with MicroPython.
Project Overview
When using Bluetooth Low Energy (BLE), it’s important to understand the roles of BLE Peripheral and BLE Controller (also referred to as the Central Device).
In this case (in the examples covered in this guide), the Raspberry Pi will be the Central device and the Pico will be the Peripheral Device.
The Pico sets up a GATT profile to allow communication with the Raspberry Pi. Once connected, it “sends” a message to the Pi, which is then displayed in the terminal. This setup can be useful for projects like using the Pico as a controller for a Raspberry Pi–powered robot.
Note: I’m putting “sends” in quotes because the Pico isn’t actually pushing data to the Pi. Instead, the Pico writes a value to a characteristic in its GATT profile. The Raspberry Pi, which is connected to the Pico over BLE, then reads that characteristic to get the data.
Below is an example of what we will be doing in this project.

- The BLE Peripheral (server) advertises its existence.
- The BLE Central Device (client) scans for BLE devices.
- When the central device finds the peripheral it is looking for, it connects to it.
- After connecting, it reads the GATT profile of the peripheral and searches for the service it is looking.
- If it finds the service, it can now interact with the characteristics. For example, reading the values.
In BLE, every service and characteristic must have a UUID (Universally Unique Identifier). These UUIDs act as unique addresses that the Raspberry Pi uses to find and connect to the Pico’s services. For example, if the Pico provides a service with read and write characteristics, each will have its own UUID. The Pi looks for these UUIDs to know how to and to communicate with them.
For a more comprehensive introduction to BLE and basic concepts such as peripheral and controller, UUIDs, GATT profile, and more, we recommend reading the introduction section of this tutorial: Raspberry Pi Pico W: Bluetooth Low Energy (BLE) with MicroPython.
Parts Required
Here’s a list of the parts required for this tutorial:
- Raspberry Pi (we’re using a Raspberry Pi 5)
- Raspberry Pi Pico W or Raspberry Pi Pico 2 W
- 1x LED
- 1x 220Ohm resistor (or similar value)
- Breadboard
- Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Preparing the Raspberry Pi
After starting with a fresh installation of Raspberry Pi OS on your RPi board, you need to follow the next instructions to enable Bluetooth on the Pi and install some essential packages.
Enabling Bluetooth on the Pi
If you are using the desktop version of Pi OS, it’s very easy to turn on Bluetooth. Go to the Bluetooth icon and click on Make Discoverable. And that’s it, you’re ready to go.
If you’re using the Lite version or if you’re in an SSH session, we will use commands to enable Bluetooth. Establish an SSH connection with your Pi.
Type the following command:
sudo systemctl start bluetooth

After that, run the following commands in order:
sudo bluetoothctl
agent on
default-agent

These commands are needed to enable Bluetooth. Once it’s done, you can exit by typing exit.
exit
Installing pipgio
Since we’ll also be controlling the Pi’s GPIOs, we need a package to control them. We’ll use pigpio. Run the following commands to install it globally and enable it on boot.
sudo apt install pigpio
sudo systemctl enable pigpiod
Creating a Virtual Environment
We’ll create a virtual environment that uses the system-wide packages like the pigpio (I’m doing it this way instead of installing it inside the virtual environment because I found some issues with controlling the GPIOs this way when using a Raspberry Pi 5).
First, we recommend creating a dedicated directory so you can keep your files organized. If you’re using the Desktop, ideally, you should go into the Documents directory and create a directory in there. For example, you can call it bluetooth_samples.
If you’re on the Lite version, you can create a directory called bluetooth_samples and then enter the directory like so:
mkdir bluetooth_samples && cd bluetooth_samples
Now, you should be in the bluetooth_samples folder.

Now, create a virtual environment called blue in that directory.
python3 -m venv blue --system-site-packages
Activate the virtual environment like so:
source blue/bin/activate
You know it’s active because the name of the virtual environment will show up before the prompt.

Installing Bleak
For the Raspberry Pi, we will need to install a BLE library that will communicate with the Pico, which is called Bleak. Install the library in the virtual environment:
pip install bleak
It should be installed after a few seconds.

Raspberry Pi Wiring Diagram
In the case of the Raspberry Pi, our example will need an LED connected to GPIO 17. You can follow the next schematic diagram.

Here, we’re connecting the longer leg of the LED to GPIO pin 17 and this will be our LED to test if the code is working properly. The shorter lead of the LED will connect to the resistor, and we will connect the ground wire to the negative rail of the breadboard where the resistor is placed.
The Pico will be mounted on the breadboard just for practical reasons, and that will be it for the wiring. If you prefer, you don’t need to mount the Pico on the breadboard.
Sending a Basic message from the Pico to Pi
In this section, we’ll show you a simple MicroPython code that will be used on the Pico to “send” messages to the Raspberry Pi.
You can call it ble_sample.py. Later, you can upload it to the Pico as main.py so that it runs without being connected to the computer.
New to BLE with the Raspberry Pi Pico? You can read our getting started guide: Raspberry Pi Pico W: Bluetooth Low Energy (BLE) with MicroPython.
Copy the following code to Thonny IDE.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/ble-raspberry-pi-and-pi-pico-w/
import asyncio
import aioble
import bluetooth
from machine import Pin
# Bluetooth configuration
_SERVICE_UUID = bluetooth.UUID(0x1848)
_WRITE_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6E) # Central writes here
_READ_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6F) # Peripheral writes message here
BLE_NAME = "Pico W Peripheral"
# Initialize LED
led = Pin("LED", Pin.OUT)
connected = False
# Register GATT server
ble_service = aioble.Service(_SERVICE_UUID)
read_characteristic = aioble.Characteristic(
ble_service, _READ_CHARACTERISTIC_UUID, read=True, notify=True
)
aioble.register_services(ble_service)
# Helper to encode the message
def _encode_message(message):
return message.encode('utf-8')
# Task to handle LED blinking and message sending
async def send_task():
global connected
message_count = 1
while True:
led.toggle()
blink = 1000 if connected else 250
if connected:
message = f"Hello from {BLE_NAME}! Count: {message_count}"
read_characteristic.write(_encode_message(message), send_update=True)
message_count += 1
print(f"Sent: {message}")
await asyncio.sleep_ms(blink)
# Serially wait for connections
async def peripheral_task():
global connected
# 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:
connected = True
print("Connection from", connection.device)
await connection.disconnected()
except Exception as e:
print("Error in peripheral_task:", e)
finally:
connected = False
print(f"{BLE_NAME} disconnected")
await asyncio.sleep_ms(100)
# Run both tasks
async def main():
t1 = asyncio.create_task(send_task())
t2 = asyncio.create_task(peripheral_task())
await asyncio.gather(t1, t2)
# Run the program
asyncio.run(main())
How Does the Code Work?
Let’s break down this code to get a better understanding.
Importing Libraries
First we import the libraries as shown below.
import asyncio
import aioble
import bluetooth
from machine import Pin
Defining UUIDs
Next, we need to define our UUIDs for the Pico. We need UUIDs for the service, write and read characteristics.
# Bluetooth configuration
_SERVICE_UUID = bluetooth.UUID(0x1848)
_WRITE_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6E) # Central writes here
_READ_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6F) # Peripheral writes message here
BLE Name
We define the name for our Raspberry Pi Pico BLE device. You can call it any other name.
BLE_NAME = "Pico W Peripheral"
Initializing the LED
The next line initializes the RPi Pico built-in LED as a GPIO output with the name led.
# Initialize LED
led = Pin("LED", Pin.OUT)
The connected variable
The connected variable will be used to keep track whether the Pico is connected to oher BLE device.
connected = False
Register the Service and Characteristic
Then, register the GATT service and characteristic.
# Register GATT server
ble_service = aioble.Service(_SERVICE_UUID)
read_characteristic = aioble.Characteristic(
ble_service, _READ_CHARACTERISTIC_UUID, read=True, notify=True
)
aioble.register_services(ble_service)
Encode the BLE Message
The message to be sent via BLE will be encoded as UTF-8. The following encode_message() function does that.
# Helper to encode the message
def _encode_message(message):
return message.encode('utf-8')
Writing to the characteristic – send_task() function
The send_task() function “sends” a message to the Raspberry Pi once connected. It will also blink the Raspberry Pi Pico built-in LED every second when connected. If not connected, the LED will blink faster, every 250 milliseconds.
async def send_task():
global connected
message_count = 1
while True:
led.toggle()
blink = 1000 if connected else 250
if connected:
message = f"Hello from {BLE_NAME}! Count: {message_count}"
read_characteristic.write(_encode_message(message), send_update=True)
message_count += 1
print(f"Sent: {message}")
await asyncio.sleep_ms(blink)
A new message is sent every second. We add a counter to the message to keep track of how many messages were sent.
message = f"Hello from {BLE_NAME}! Count: {message_count}"
This is the line that “sends” a new message.
read_characteristic.write(_encode_message(message), send_update=True)
The message_count is incremented in each loop.
message_count += 1
Notice that the send_task is an asynchronous function. For BLE handling, it’s better to use asynchronous programming to avoid timing issues. If you want to learn more about asynchronous programming with the Raspberry Pi Pico using MicroPython, we recommend the following tutorial:
Advertise the Pico as a BLE Service – peripheral_task()
Besides writing to the temperature characteristic, we also need to advertise the Raspberry Pi Pico as a BLE service. For that, we use the peripheral_task() function that sets up the Pico as a peripheral device and starts advertising.
We also get and print the Pico MAC address—we’ll need it later on the Raspberry Pi side to connect to the Pico.
# Serially wait for connections
async def peripheral_task():
global connected
# 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:
connected = True
print("Connection from", connection.device)
await connection.disconnected()
except Exception as e:
print("Error in peripheral_task:", e)
finally:
connected = False
print(f"{BLE_NAME} disconnected")
await asyncio.sleep_ms(100)
Main Function
Finally, we create an asynchronou main() function, where we’ll write the base for our code. We create two asynchronous tasks: one for advertising and another to send new messages every second and blink the on-board LED. Then, we gather and run the tasks asynchronously by asynchronously calling the main() function.
# Run both tasks
async def main():
t1 = asyncio.create_task(send_task())
t2 = asyncio.create_task(peripheral_task())
await asyncio.gather(t1, t2)
# Run the program
asyncio.run(main())
Running the Code
Run the code on the Pico by pressing the green play button on Thonny or we can press F5 on the keyboard. Once it’s done, it’ll display the MAC address of the Pico.

Raspberry Pi – Receive the RPi Pico Messages
On the Raspberry Pi, create a new file with the code below. You can call it pi_led_receive.py. The file should be located inside the folder we created previously called bluetooth_samples — the same place where we created our virtual environment.
There are many ways to create and run files on the Pi. I like to use Remote-SSH on VS Code. This extension allows you to establish an SSH connection with your Pi, create files, write code, and execute it directly on your Raspberry Pi board from your computer using the VS Code interface. Learn more here: Programming Raspberry Pi Remotely using VS Code (Remote-SSH).
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/ble-raspberry-pi-and-pi-pico-w/
import asyncio
from bleak import BleakClient, uuids
from gpiozero import LED
connected = False
led = LED(17)
# Replace with the MAC address of your Pico
pico_address = "FF:FF:FF:FF:FF:FF"
# Service UUID (0x1848)
SERVICE_UUID = uuids.normalize_uuid_16(0x1848)
WRITE_CHARACTERISTIC_UUID = uuids.normalize_uuid_16(0x2A6E) # Central writes here
READ_CHARACTERISTIC_UUID = uuids.normalize_uuid_16(0x2A6F) # Central reads here
async def receive_data_task(client):
"""Receive data from the peripheral device."""
while True:
try:
response = await client.read_gatt_char(READ_CHARACTERISTIC_UUID)
print(f"Central received: {response.decode('utf-8')}")
await asyncio.sleep(1)
except Exception as e:
print(f"Error receiving data: {e}")
break
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
"""Connect to the peripheral and manage data exchange."""
print(f"Connecting to {address}...")
async with BleakClient(address) as client:
connected = client.is_connected
print(f"Connected: {connected}")
# Create tasks for sending and receiving data
tasks = [
asyncio.create_task(receive_data_task(client)),
asyncio.create_task(blink_task())
]
await asyncio.gather(*tasks)
connected = False
# Run the connection and communication
loop = asyncio.get_event_loop()
loop.run_until_complete(connect_and_communicate(pico_address))
Before running the code, you need to modify the following line with the Bluetooth MAC address of the Pico. You should have gotten it in the previous example.
# Replace with the MAC address of your Pico
pico_address = "2C:CF:67:B6:D7:4C"
How Does the Code Work?
Let’s now take a quick look at the code to see how it works.
First, import the required libraries. We’re including bleak for the Bluetooth functions.
import asyncio
from bleak import BleakClient, uuids
from gpiozero import LED
We have a boolean variable to keep track of whether we’re connected to another Bluetooth peripheral or not.
connected = False
We define the service and characteristics’ UUIDs. These are the service and characteristics of the Pico that we’ll search for to read the message sent from the Pico. So, these must be the same of the Pico.
# Service UUID (0x1848)
SERVICE_UUID = uuids.normalize_uuid_16(0x1848)
WRITE_CHARACTERISTIC_UUID = uuids.normalize_uuid_16(0x2A6E) # Central writes here
READ_CHARACTERISTIC_UUID = uuids.normalize_uuid_16(0x2A6F) # Central reads here
Then, we have an asynchronous function called receive_data_task() that waits to read the characteristic value of the peripheral device. It reads the characteristic every second.
async def receive_data_task(client):
"""Receive data from the peripheral device."""
while True:
try:
response = await client.read_gatt_char(READ_CHARACTERISTIC_UUID)
print(f"Central received: {response.decode('utf-8')}")
await asyncio.sleep(1)
except Exception as e:
print(f"Error receiving data: {e}")
break
We have another task called blink_task() that will run at the same time. This task will blink the LED connected to GPIO 17 on the Raspberry Pi. It will run every second if we are connected to the peripheral device.
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)
Finally, the connect_and_communicate() function tries to connect to the peripheral device with the MAC address we defined earlier. Once it is connected, we create and gather the tasks to receive data and blink the LED.
async def connect_and_communicate(address):
global connected
"""Connect to the peripheral and manage data exchange."""
print(f"Connecting to {address}...")
async with BleakClient(address) as client:
connected = client.is_connected
print(f"Connected: {connected}")
# Create tasks for sending and receiving data
tasks = [
asyncio.create_task(receive_data_task(client)),
asyncio.create_task(blink_task())
]
await asyncio.gather(*tasks)
connected = False
Finally, we asynchronously run the tasks in a loop.
# Run the connection and communication
loop = asyncio.get_event_loop()
loop.run_until_complete(connect_and_communicate(pico_address))
Running the Python Code
After creating the pi_led_receive.py file with the code we’ve shared previously and inserting your Pico MAC address, you can run it on your Raspberry Pi board. We’re using the Remote-SSH extension on VS Code, but you can simply create a new file using the nano command.
After saving the file, you can run it. But first, make sure you’re in the virtual environment we created previously. If you’ve exited the virtual environment, you can activate it again like so:
source blue/bin/activate
Finally, you can run your Python program like this:
python3 pi_led_receive.py
Demonstration
The code will start running, and after a few seconds it will connect to the Raspberry Pi Pico (make sure the RPi Pico is running the code on Thonny IDE).
It will start receiving the Pico’s messages.

At the same time, the LED connected to GPIO 17 will start blinking every second.

And that’s it. Now, you have the Raspberry Pi reading data from the Raspberry Pi Pico via BLE.
You can also check the sent messages on the shell of Thonny IDE.

Sending Current Time from the Pico to Pi Example
Instead of sending a message without meaning, we can easily change the code to send sensor readings or commands, for example. In this section, we’ll show you how you can easily change the previous example to send different data. In this case, we’ll send the current time from the Pico to the Pi.
The code for the Pi will remain the same. For the Pico, we need to make some slight changes. This is the code for the Pico.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/ble-raspberry-pi-and-pi-pico-w/
import asyncio
import aioble
import bluetooth
from machine import Pin
import time
# Bluetooth configuration
_SERVICE_UUID = bluetooth.UUID(0x1848)
_WRITE_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6E) # Central writes here
_READ_CHARACTERISTIC_UUID = bluetooth.UUID(0x2A6F) # Peripheral writes message here
BLE_NAME = "Pico W Peripheral"
# Initialize LED
led = Pin("LED", Pin.OUT)
connected = False
# Register GATT server
ble_service = aioble.Service(_SERVICE_UUID)
read_characteristic = aioble.Characteristic(
ble_service, _READ_CHARACTERISTIC_UUID, read=True, notify=True
)
aioble.register_services(ble_service)
# Helper to encode the message
def _encode_message(message):
return message.encode('utf-8')
# Task to handle LED blinking and message sending
async def send_task():
global connected
while True:
led.toggle()
blink = 1000 if connected else 250
if connected:
today = time.localtime() # get the current time
message = f"{today}"
read_characteristic.write(_encode_message(message), send_update=True)
print(f"Sent: {message}")
await asyncio.sleep_ms(blink)
# Serially wait for connections
async def peripheral_task():
global connected
# 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:
connected = True
print("Connection from", connection.device)
await connection.disconnected()
except Exception as e:
print("Error in peripheral_task:", e)
finally:
connected = False
print(f"{BLE_NAME} disconnected")
await asyncio.sleep_ms(100)
# Run both tasks
async def main():
t1 = asyncio.create_task(send_task())
t2 = asyncio.create_task(peripheral_task())
await asyncio.gather(t1, t2)
# Run the program
asyncio.run(main())
First, we will have to add the library for time.
import time
Then, we need to modify the send_task() function. Basically, once connected, we get the current time and save it in the today message.
today = time.localtime()
The local time will be our message.
message = f"{today}"
Then, we simply need to write the current time on the characteristic like so:
read_characteristic.write(encode_message(message), send_update=True)
Now, it’s easy to understand that you can easily modify the code to send any other useful data like sensor readings or commands to control outputs, for example.
Demonstration
On the Raspberry Pi side, it should have disconnected from the Pico when you run this new code. You need to run the Python code on the Raspberry Pi again right after running the code on the Pico.
After a while, it will be connected and start sending the messages.
On the Raspberry Pi side, it will receive the time sent from the Pico. The timestamp should be printed in the terminal window.

Wrapping up
In this tutorial, we learned the basics of BLE and how to set up the Pi to interact with the Pico.
We’re planning to create more tutorials about this subject. Specifically, cover bidirectional communication so that both devices can send and receive data at the same time.
Meanwhile, here are some challenges if you’d like to take this example further:
- Have the Pi read the values of the Pico’s internal temperature sensor.
- Add an external temperature sensor like the BME280 to the Pico and have the Pi read back the temperature values.
- For an added challenge, add an I2C-based LCD screen on the Pi to display the temperatures rather than simply printing the values in the terminal.
We hope you’ve found this tutorial useful.
Special thanks to our reader Edgardo Peregrino, who created and wrote the layout for this tutorial as well as the example codes.
Edgardo Peregrino is the author of “Programming Raspberry Pi in 30 Days” and “Cloud Powered Robotics with Raspberry Pi” and writer of several articles especially in Servo Magazine. You can check his work here on its GitHub page or YouTube channel (LinuxRobotGeek).
Learn more about the Raspberry Pi Pico and the Raspberry Pi:
- All our Raspberry Pi Tutorials
- Learn Raspberry Pi Pico with MicroPython eBook
- All our Raspberry Pi Pico Tutorials
Thanks for reading.
Hello, a very good tutorial, works perfectly, a question, how to control the LED of my PI with an android application and BLE, using python? Thank you