Advertise, Serve, Publish
Host a local BLE peripheral, serve GATT characteristics, and publish value updates to connected clients.
This is the peripheral-mode walkthrough: this host advertises, serves a GATT table, and publishes characteristic updates to connected clients.
SimpleBLE::Peripheral is a remote device discovered while acting as a central. SimpleBLE::Local::Peripheral is this host. Create the local peripheral from the same Adapter you would use to scan.
Available on all supported platforms
The C++ API can host a local peripheral on every supported platform. Native Android applications must provide their application context with SimpleBLE::Advanced::Android::set_context() before creating the local peripheral. SimpleDroidBLE handles the context automatically and provides a Kotlin API.
If you still need to install SimpleBLE or create the CMake project, start with Getting Started. The Advertise recipe is the short form of this flow.
Lifecycle
Configure first, then start:
- Create a local peripheral from an adapter.
- Set advertisement data and add services and characteristics.
- Call
start()to serve the GATT application and begin advertising. - Update characteristic values and observe clients while started.
- Call
stop()when finished.
Services, characteristics, and advertising data must be configured before start() and cannot be changed while the peripheral is started. set_value() and the characteristic callbacks are the supported way to change data after that. Starting twice is a no-op; stop() while already stopped is also a no-op. Destroying the object stops it.
auto adapters = SimpleBLE::Adapter::get_adapters();
auto adapter = adapters.front();
auto peripheral = adapter.create_local_peripheral();
peripheral.add_advertised_service("12345678-1234-5678-1234-56789abcdef0");
auto service = peripheral.add_service("12345678-1234-5678-1234-56789abcdef0");
auto characteristic = service.add_characteristic(
"12345678-1234-5678-1234-56789abcdef1",
{SimpleBLE::Local::CharacteristicCapability::READ,
SimpleBLE::Local::CharacteristicCapability::WRITE_REQUEST,
SimpleBLE::Local::CharacteristicCapability::NOTIFY});
characteristic.set_value(SimpleBLE::ByteArray("ready"));
peripheral.start();is_started() reports whether the GATT application is registered. is_advertising() reports whether the advertisement is currently active.
Advertised services
Add configured service UUIDs to the advertisement directly on the local peripheral:
peripheral.add_advertised_service("12345678-1234-5678-1234-56789abcdef0");If no advertised services are added explicitly, SimpleBLE advertises the service UUIDs configured on the local peripheral when the platform supports doing so. Pass a vector to add several UUIDs in one call.
The portable advertisement does not select a local name. Windows controls the name in its discoverable advertisement, and Android includes the adapter's system Bluetooth name. Neither platform exposes an arbitrary per-advertisement name through its public GATT advertising API.
BlueZ and CoreBluetooth do support an advertisement-specific name. Configure that platform capability before start() without changing the adapter alias, system Bluetooth name, or GAP Device Name:
#include <simpleble/Advanced.h>
#if defined(__linux__) && !defined(__ANDROID__)
SimpleBLE::Advanced::Linux::set_advertisement_local_name(peripheral, "SimpleBLE Peripheral");
#elif TARGET_OS_OSX
SimpleBLE::Advanced::MacOS::set_advertisement_local_name(peripheral, "SimpleBLE Peripheral");
#elif TARGET_OS_IOS
SimpleBLE::Advanced::iOS::set_advertisement_local_name(peripheral, "SimpleBLE Peripheral");
#endifCoreBluetooth advertises data on a best-effort basis, and iOS omits the local name while the app is in the background. BlueZ may shorten a name that does not fit. Pass std::nullopt to remove an override.
Services and characteristics
add_service() adds a primary GATT service. add_characteristic() takes a std::set of capabilities, so each flag is unique:
| Capability | Remote centrals can |
|---|---|
READ | Read the characteristic |
WRITE_REQUEST | Write with a response |
WRITE_COMMAND | Write without a response |
NOTIFY | Subscribe to notifications |
INDICATE | Subscribe to indications |
A characteristic needs at least one capability. Pass the set as an initializer list:
auto characteristic = service.add_characteristic(
CHARACTERISTIC_UUID,
{SimpleBLE::Local::CharacteristicCapability::READ,
SimpleBLE::Local::CharacteristicCapability::WRITE_REQUEST,
SimpleBLE::Local::CharacteristicCapability::WRITE_COMMAND,
SimpleBLE::Local::CharacteristicCapability::NOTIFY,
SimpleBLE::Local::CharacteristicCapability::INDICATE});Values and callbacks
Without a read callback, reads return value(). Incoming writes always update value() before the write callback runs.
Optional callbacks customize or observe that behavior:
set_callback_on_read— return the bytes to send for this readset_callback_on_write— observe the bytes after they have been stored. Callset_value()from the callback only when you want to publish or echo that value to subscribed clients.
characteristic.set_callback_on_write([&characteristic](SimpleBLE::ByteArray value) {
std::cout << "Write: " << value << std::endl;
// Echo the written value to subscribed clients.
characteristic.set_value(std::move(value));
});Publish updates
set_value() is how this host publishes a new characteristic value. When the characteristic has NOTIFY or INDICATE capability, SimpleBLE also pushes that value to subscribed clients on a best-effort basis.
characteristic.set_value(SimpleBLE::ByteArray("tick"));set_callback_on_subscribed runs when the first client subscribes. set_callback_on_unsubscribed runs when the last client unsubscribes.
characteristic.set_callback_on_subscribed([]() { std::cout << "Client subscribed." << std::endl; });
characteristic.set_callback_on_unsubscribed([]() { std::cout << "Client unsubscribed." << std::endl; });Use publish for this path. start() serves the GATT application and begins advertising; it is not the same as publishing a characteristic value.
Observe clients
Register callbacks before start() if you want the first connection:
peripheral.set_callback_on_client_connected(
[](SimpleBLE::BluetoothAddress address) { std::cout << "Client connected: " << address << std::endl; });
peripheral.set_callback_on_client_disconnected(
[](SimpleBLE::BluetoothAddress address) { std::cout << "Client disconnected: " << address << std::endl; });On platforms that do not expose a Bluetooth address, the value is a platform-specific identifier for the remote client.
CoreBluetooth does not expose peripheral-role connection events on macOS. SimpleBLE reports a client as connected when it first subscribes to a characteristic and disconnected when its final subscription ends.
On Windows, WinRT exposes the client session after a client first reads, writes, or subscribes to a hosted characteristic. A connection that performs no GATT operation may not produce a client-connected callback.
Callbacks follow the same threading rules as the rest of SimpleBLE. See Concurrency.
Scan and advertise together
SimpleBLE permits the same adapter to scan while a local peripheral is started when the platform Bluetooth stack and controller support concurrent scanning and advertising. On Linux, BlueZ and the controller must support both operations at the same time. On Android, support depends on the phone's controller and vendor Bluetooth stack. Incoming clients of the local peripheral are reported through the client callbacks above. They are not devices you should connect() to as a central.
Full application
The repository example examples/simpleble/src/peripheral.cpp is a complete program. A compact version:
#include <simpleble/SimpleBLE.h>
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdlib>
#include <iostream>
#include <thread>
#include <utility>
namespace {
constexpr auto SERVICE_UUID = "12345678-1234-5678-1234-56789abcdef0";
constexpr auto CHARACTERISTIC_UUID = "12345678-1234-5678-1234-56789abcdef1";
std::atomic_bool running{true};
void signal_handler(int) { running = false; }
} // namespace
int main() {
std::signal(SIGINT, signal_handler);
auto adapters = SimpleBLE::Adapter::get_adapters();
if (adapters.empty()) {
std::cerr << "No Bluetooth adapters found." << std::endl;
return EXIT_FAILURE;
}
auto adapter = adapters.front();
auto peripheral = adapter.create_local_peripheral();
peripheral.add_advertised_service(SERVICE_UUID);
auto service = peripheral.add_service(SERVICE_UUID);
auto characteristic = service.add_characteristic(
CHARACTERISTIC_UUID,
{SimpleBLE::Local::CharacteristicCapability::READ, SimpleBLE::Local::CharacteristicCapability::WRITE_REQUEST,
SimpleBLE::Local::CharacteristicCapability::NOTIFY});
characteristic.set_value(SimpleBLE::ByteArray("ready"));
characteristic.set_callback_on_write([&characteristic](SimpleBLE::ByteArray value) {
std::cout << "Write: " << value << std::endl;
// Echo the written value to subscribed clients.
characteristic.set_value(std::move(value));
});
peripheral.set_callback_on_client_connected(
[](SimpleBLE::BluetoothAddress address) { std::cout << "Client connected: " << address << std::endl; });
peripheral.start();
std::cout << "Local peripheral is advertising. Press Ctrl+C to stop." << std::endl;
while (running) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
characteristic.set_callback_on_write({});
peripheral.stop();
return EXIT_SUCCESS;
}Build it with the other SimpleBLE examples:
cmake -S <path-to-simpleble>/examples/simpleble -B build_simpleble_examples -DSIMPLEBLE_LOCAL=ON
cmake --build build_simpleble_examples -j7Where to next?
- Advertise for a shorter recipe form of this flow
- Scan, Connect, Read to use the same adapter as a central
- API reference for
SimpleBLE::Localtypes - Platform notes for platform-specific behavior
- Examples for the programs in the repository
