Getting Started
Build your first SimpleBLE app by scanning, connecting, and reading a characteristic.
SimpleBLE is a cross-platform C++ library for Bluetooth Low Energy. In this quickstart, you will build a small interactive console application that discovers nearby BLE devices, lets the user pick one, inspects its services and characteristics, and reads data from a selected characteristic.
By the end, your app will:
- Discover a Bluetooth adapter
- Scan for nearby peripherals
- Select a device to connect to
- List readable characteristics
- Read a characteristic value multiple times
If you are new to BLE or platform-specific permissions, it is worth taking a quick look at Bluetooth LE Basics and Bluetooth Permissions before you begin.
Prerequisites
Before we start building the sample application, make sure your machine and development environment are ready:
- A C++17-compatible compiler
- CMake 3.21 or newer
- A machine with Bluetooth Low Energy support enabled
- The platform dependencies described in the usage guide
If you are developing on macOS, iOS, or Android, make sure your app has the required Bluetooth permission entries before testing.
Install SimpleBLE
Start by cloning the SimpleBLE repository locally:
git clone https://github.com/simpleble/simpleble.git
cd simplebleWith the source available locally, the next step is to build and install the library:
cmake -S . -B build_simpleble
cmake --build build_simpleble -j7
cmake --install build_simplebleOn Linux and macOS, you may need elevated privileges for the install step:
sudo cmake --install build_simplebleOnce that finishes, SimpleBLE is installed on your machine and ready to be used from your own CMake project. If you need platform-specific setup details, refer to the usage page and platform notes.
Create your application
Now that SimpleBLE is installed, we can create a small sample project that links against it and uses it to talk to a BLE device. Start with this structure:
my-simpleble-app/
|-- CMakeLists.txt
|-- src/
|-- main.cppAdd CMakeLists.txt
In the project root, create CMakeLists.txt:
cmake_minimum_required(VERSION 3.21)
project(simpleble_quickstart LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(simpleble REQUIRED CONFIG)
add_executable(simpleble_quickstart src/main.cpp)
target_link_libraries(simpleble_quickstart PRIVATE simpleble::simpleble)This gives your sample project a standard CMake setup and tells it to locate the installed SimpleBLE package and link it to your executable.
Build a BLE reader
Now that the project structure is ready, we will implement the BLE reader step by step, starting with basic Bluetooth access and moving through device discovery, connection, and finally reading data.
Step 1: Initialize the Bluetooth adapter
We will begin by verifying that Bluetooth is available and that the operating system has granted your app access to it:
#include <iostream>
#include <simpleble/SimpleBLE.h>
int main() {
if (!SimpleBLE::Adapter::bluetooth_enabled()) {
std::cerr << "Bluetooth is not enabled or permission has not been granted." << std::endl;
return EXIT_FAILURE;
}
std::cout << "Bluetooth is available." << std::endl;
return EXIT_SUCCESS;
}Step 2: Get a Bluetooth adapter
With Bluetooth access confirmed, we need to discover the adapters available on the machine. SimpleBLE manages multiple backends representing different under-the-hood implementations (such as BlueZ, CoreBluetooth, WinRT, or Dongl), but provides a consolidated method to easily fetch all available adapters across every active backend. To keep things simple, we will just use the first adapter that we find:
auto adapters = SimpleBLE::Adapter::get_adapters();
if (adapters.empty()) {
std::cerr << "No Bluetooth adapters found." << std::endl;
return EXIT_FAILURE;
}
auto adapter = adapters[0];
std::cout << "Using adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl;Step 3: Scan for peripherals
Now that we have an active adapter, we can use it to scan for nearby devices. We'll use callbacks to track discovered devices and store connectable ones in a vector:
std::vector<SimpleBLE::Peripheral> peripherals;
adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; });
adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; });
adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) {
std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
if (peripheral.is_connectable()) {
peripherals.push_back(peripheral);
}
});
adapter.scan_for(5000);Step 4: Select a device
Once the scan is complete, we display the discovered connectable devices and ask the user to select one:
if (peripherals.empty()) {
std::cerr << "No connectable peripherals found." << std::endl;
return EXIT_FAILURE;
}
std::cout << "Connectable devices:" << std::endl;
for (std::size_t i = 0; i < peripherals.size(); i++) {
std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]" << std::endl;
}
std::size_t peripheral_index = 0;
std::cout << "Select a device to connect to (0-" << peripherals.size() - 1 << "): ";
std::cin >> peripheral_index;
if (!std::cin || peripheral_index >= peripherals.size()) {
std::cerr << "Invalid peripheral selection." << std::endl;
return EXIT_FAILURE;
}
auto peripheral = peripherals[peripheral_index];Step 5: Connect and discover characteristics
Once a peripheral has been selected, the next step is to establish a connection and inspect its services to find characteristics that support data reads:
std::cout << "Connecting to " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
peripheral.connect();
std::vector<std::pair<SimpleBLE::BluetoothUUID, SimpleBLE::BluetoothUUID>> readable_characteristics;
for (const auto& service : peripheral.services()) {
for (const auto& characteristic : service.characteristics()) {
if (characteristic.can_read()) {
readable_characteristics.emplace_back(service.uuid(), characteristic.uuid());
}
}
}
if (readable_characteristics.empty()) {
std::cerr << "The peripheral has no readable characteristics." << std::endl;
peripheral.disconnect();
return EXIT_FAILURE;
}
std::cout << "Readable characteristics:" << std::endl;
for (std::size_t i = 0; i < readable_characteristics.size(); i++) {
std::cout << "[" << i << "] " << readable_characteristics[i].first << " " << readable_characteristics[i].second << std::endl;
}Step 6: Read data from the device
For the final step, we will let the user choose one of the discovered readable characteristics and read its value five times:
std::size_t characteristic_index = 0;
std::cout << "Select a characteristic to read (0-" << readable_characteristics.size() - 1 << "): ";
std::cin >> characteristic_index;
if (!std::cin || characteristic_index >= readable_characteristics.size()) {
std::cerr << "Invalid characteristic selection." << std::endl;
peripheral.disconnect();
return EXIT_FAILURE;
}
const auto& [service_uuid, characteristic_uuid] = readable_characteristics[characteristic_index];
for (std::size_t i = 0; i < 5; i++) {
SimpleBLE::ByteArray data = peripheral.read(service_uuid, characteristic_uuid);
std::cout << "Read " << i + 1 << ": " << data << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
peripheral.disconnect();Full Application Code
Here is the complete src/main.cpp file combining all the steps above:
#include <chrono>
#include <iostream>
#include <thread>
#include <utility>
#include <vector>
#include <simpleble/SimpleBLE.h>
using namespace std::chrono_literals;
int main() {
if (!SimpleBLE::Adapter::bluetooth_enabled()) {
std::cerr << "Bluetooth is not enabled or permission has not been granted." << std::endl;
return EXIT_FAILURE;
}
auto adapters = SimpleBLE::Adapter::get_adapters();
if (adapters.empty()) {
std::cerr << "No Bluetooth adapters found." << std::endl;
return EXIT_FAILURE;
}
auto adapter = adapters[0];
std::cout << "Using adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl;
std::vector<SimpleBLE::Peripheral> peripherals;
adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; });
adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; });
adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) {
std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
if (peripheral.is_connectable()) {
peripherals.push_back(peripheral);
}
});
adapter.scan_for(5000);
if (peripherals.empty()) {
std::cerr << "No connectable peripherals found." << std::endl;
return EXIT_FAILURE;
}
std::cout << "Connectable devices:" << std::endl;
for (std::size_t i = 0; i < peripherals.size(); i++) {
std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]"
<< std::endl;
}
std::size_t peripheral_index = 0;
std::cout << "Select a device to connect to (0-" << peripherals.size() - 1 << "): ";
std::cin >> peripheral_index;
if (!std::cin || peripheral_index >= peripherals.size()) {
std::cerr << "Invalid peripheral selection." << std::endl;
return EXIT_FAILURE;
}
auto peripheral = peripherals[peripheral_index];
std::cout << "Connecting to " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
peripheral.connect();
std::vector<std::pair<SimpleBLE::BluetoothUUID, SimpleBLE::BluetoothUUID>> readable_characteristics;
for (const auto& service : peripheral.services()) {
for (const auto& characteristic : service.characteristics()) {
if (characteristic.can_read()) {
readable_characteristics.emplace_back(service.uuid(), characteristic.uuid());
}
}
}
if (readable_characteristics.empty()) {
std::cerr << "The peripheral has no readable characteristics." << std::endl;
peripheral.disconnect();
return EXIT_FAILURE;
}
std::cout << "Readable characteristics:" << std::endl;
for (std::size_t i = 0; i < readable_characteristics.size(); i++) {
std::cout << "[" << i << "] " << readable_characteristics[i].first << " "
<< readable_characteristics[i].second << std::endl;
}
std::size_t characteristic_index = 0;
std::cout << "Select a characteristic to read (0-" << readable_characteristics.size() - 1 << "): ";
std::cin >> characteristic_index;
if (!std::cin || characteristic_index >= readable_characteristics.size()) {
std::cerr << "Invalid characteristic selection." << std::endl;
peripheral.disconnect();
return EXIT_FAILURE;
}
const auto& [service_uuid, characteristic_uuid] = readable_characteristics[characteristic_index];
for (std::size_t i = 0; i < 5; i++) {
SimpleBLE::ByteArray data = peripheral.read(service_uuid, characteristic_uuid);
std::cout << "Read " << i + 1 << ": " << data << std::endl;
std::this_thread::sleep_for(1s);
}
peripheral.disconnect();
return EXIT_SUCCESS;
}How the example works
Now that the full sample is in place, let's walk through the complete flow once more:
SimpleBLE::Adapter::bluetooth_enabled()verifies that Bluetooth is available and that the application has the permissions it needs.SimpleBLE::Adapter::get_adapters()aggregates and returns the Bluetooth adapters available across all native backends on the system. To keep things simple, the program uses the first one it finds.adapter.scan_for(5000)scans for nearby peripherals for five seconds. Theset_callback_on_scan_found()callback prints each discovered device and stores connectable ones.- After selecting a peripheral,
peripheral.connect()establishes a connection and allows service discovery. peripheral.services()returns the GATT services exposed by the device. The example walks through each service and characteristic, keeping only those wherecharacteristic.can_read()is true.- Finally,
peripheral.read(service_uuid, characteristic_uuid)reads the selected characteristic five times, once per second.
This scan -> connect -> discover -> read pattern is one of the most common SimpleBLE workflows, and it is a good foundation for building more advanced applications.
Build and run your application
With both files in place, go to your project directory and run:
cmake -S . -B build
cmake --build build
./build/simpleble_quickstartWhen the program starts, it will guide you through the full interaction:
- Scan for nearby peripherals
- Ask you which device to connect to
- Show the readable characteristics it discovered
- Read the selected characteristic five times
Where to next?
Once you are comfortable with this first read flow, these are good places to continue:
- API reference for the complete C++ surface area
- Examples for write, notify, and connect flows
- FAQ for common questions and troubleshooting tips
