SimpleBLE
Learn Bluetooth Low EnergyModule 1: First contact

BLE Central and Peripheral Roles

What centrals, peripherals, observers and broadcasters do in Bluetooth Low Energy, and which role your computer and your devices play.

Every BLE device plays a role. The role decides who announces itself, who listens, and who starts a connection.

The roles

  • Peripheral. Advertises its presence and accepts a connection. A heart rate strap, a smart lock or a sensor is a peripheral.
  • Central. Scans for advertisements and connects to the peripheral it wants. A phone or a computer is usually the central.
  • Broadcaster. Advertises and never accepts a connection. Beacons are broadcasters: everything they have to say fits in the advertisement.
  • Observer. Scans and never connects. A program that only collects beacon readings is an observer.

The roles describe what a device is doing, not what it is. A phone is a central when it talks to a smartwatch and a peripheral when it unlocks a door. Your computer can take either role with SimpleBLE: in this module it scans as a central, and in Module 2 it advertises as a peripheral.

Peripheraladvertises, serves dataCentralscans, connectsAdvertisementAdvertisementConnection requestRead or writeNotification
Advertisements travel one way until the central connects. After that, data moves in both directions.

A peripheral advertises until a central connects. Once connected, the central acts as the GATT client: it reads and writes the peripheral's data. The peripheral acts as the GATT server: it holds the data and sends notifications when values change. Module 3 covers that exchange.

Before the connection, everything the central knows about a peripheral comes from its advertisements, including whether it accepts connections at all. The BLE Advertisement Builder shows what a device puts in those few bytes.

Count the roles around you

This program scans for five seconds as a central. It lists the peripherals it heard, the devices that accept connections, and counts the broadcasters, the devices that only advertise.

#include <iostream>
#include <simpleble/SimpleBLE.h>

int main() {
    auto adapter = SimpleBLE::Adapter::get_adapters().at(0);
    adapter.scan_for(5000);

    auto results = adapter.scan_get_results();
    size_t peripherals = 0;
    for (auto& device : results) {
        if (device.is_connectable()) {
            std::cout << "Peripheral: " << device.identifier() << " [" << device.address() << "]" << std::endl;
            peripherals++;
        }
    }

    std::cout << "Peripherals (accept connections): " << peripherals << std::endl;
    std::cout << "Broadcasters (advertise only): " << results.size() - peripherals << std::endl;
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <simplecble/simplecble.h>

int main(void) {
    simpleble_error_t* error = NULL;
    simpleble_adapter_list_t adapters = NULL;
    simpleble_adapter_get_adapters(&adapters, &error);
    simpleble_adapter_t adapter = error ? NULL : simpleble_adapter_list_get(adapters, 0, &error);
    simpleble_adapter_list_release(adapters);

    simpleble_peripheral_list_t results = NULL;
    size_t count = 0;
    if (!error) simpleble_adapter_scan_for(adapter, 5000, &error);
    if (!error) count = simpleble_adapter_scan_get_results(adapter, &results, &error);

    size_t peripherals = 0;
    for (size_t i = 0; i < count && !error; i++) {
        simpleble_peripheral_t device = simpleble_peripheral_list_get(results, i, &error);
        if (error) break;

        if (simpleble_peripheral_is_connectable(device, &error)) {
            char* identifier = simpleble_peripheral_identifier(device, &error);
            char* address = error ? NULL : simpleble_peripheral_address(device, &error);
            if (!error) printf("Peripheral: %s [%s]\n", identifier, address);
            simpleble_free(identifier);
            simpleble_free(address);
            peripherals++;
        }
        simpleble_peripheral_release_handle(device);
    }

    if (!error) {
        printf("Peripherals (accept connections): %zu\n", peripherals);
        printf("Broadcasters (advertise only): %zu\n", count - peripherals);
    }

    int result = error ? EXIT_FAILURE : EXIT_SUCCESS;
    if (error) fprintf(stderr, "%s\n", simpleble_error_message(error));
    simpleble_error_release(&error);
    simpleble_peripheral_list_release(results);
    simpleble_adapter_release_handle(adapter);
    return result;
}
import simplepyble

adapter = simplepyble.Adapter.get_adapters()[0]
adapter.scan_for(5000)

results = adapter.scan_get_results()
peripherals = 0
for device in results:
    if device.is_connectable():
        print(f"Peripheral: {device.identifier()} [{device.address()}]")
        peripherals += 1

print(f"Peripherals (accept connections): {peripherals}")
print(f"Broadcasters (advertise only): {len(results) - peripherals}")
import java.util.List;

import org.simplejavable.Adapter;
import org.simplejavable.Peripheral;

public class Roles {
    public static void main(String[] args) throws Exception {
        Adapter adapter = Adapter.getAdapters().get(0);
        adapter.scanFor(5000);

        List<Peripheral> results = adapter.scanGetResults();
        int peripherals = 0;
        for (Peripheral device : results) {
            if (device.isConnectable()) {
                System.out.println("Peripheral: " + device.getIdentifier() + " [" + device.getAddress() + "]");
                peripherals++;
            }
        }

        System.out.println("Peripherals (accept connections): " + peripherals);
        System.out.println("Broadcasters (advertise only): " + (results.size() - peripherals));
    }
}
use std::time::Duration;

use simplersble::Adapter;

fn main() {
    let adapter = Adapter::get_adapters().unwrap().remove(0);
    adapter.scan_for(Duration::from_secs(5)).unwrap();

    let results = adapter.scan_get_results().unwrap();
    let mut peripherals = 0;
    for device in &results {
        if device.is_connectable().unwrap() {
            println!("Peripheral: {} [{}]", device.identifier().unwrap(), device.address().unwrap());
            peripherals += 1;
        }
    }

    println!("Peripherals (accept connections): {}", peripherals);
    println!("Broadcasters (advertise only): {}", results.len() - peripherals);
}
import { Adapter } from 'simplejsble';

const [adapter] = await Adapter.getAdapters();
await adapter.scanFor(5000);

const results = adapter.scanGetResults();
let peripherals = 0;
for (const device of results) {
  if (device.isConnectable()) {
    console.log(`Peripheral: ${device.identifier()} [${device.address()}]`);
    peripherals++;
  }
}

console.log(`Peripherals (accept connections): ${peripherals}`);
console.log(`Broadcasters (advertise only): ${results.length - peripherals}`);
import { Adapter } from 'simplernble';

export async function countRoles() {
  const [adapter] = await Adapter.getAdapters();
  await adapter.scanFor(5000);

  const results = adapter.scanGetResults();
  let peripherals = 0;
  for (const device of results) {
    if (device.isConnectable()) {
      console.log(`Peripheral: ${device.identifier()} [${device.address()}]`);
      peripherals++;
    }
  }

  console.log(`Peripherals (accept connections): ${peripherals}`);
  console.log(`Broadcasters (advertise only): ${results.length - peripherals}`);
}
import kotlinx.coroutines.runBlocking
import org.simplektble.Adapter

fun main() = runBlocking {
    val adapter = Adapter.getAdapters().first()
    adapter.scanFor(5000)

    val results = adapter.scanGetResults()
    var peripherals = 0
    for (device in results) {
        if (device.isConnectable) {
            println("Peripheral: ${device.identifier} [${device.address}]")
            peripherals++
        }
    }

    println("Peripherals (accept connections): $peripherals")
    println("Broadcasters (advertise only): ${results.size - peripherals}")
}
import SimpleSwiftBLE

let adapter = try Adapter.getAdapters().first!
try await adapter.scanFor(timeoutMs: 5000)

let results = try adapter.scanGetResults()
var peripherals = 0
for device in results where device.isConnectable {
    print("Peripheral: \(device.identifier) [\(device.address)]")
    peripherals += 1
}

print("Peripherals (accept connections): \(peripherals)")
print("Broadcasters (advertise only): \(results.count - peripherals)")
import 'package:simpledartble/simpledartble.dart';

Future<void> main() async {
  final adapter = (await Adapter.getAdapters()).first;
  await adapter.scanFor(const Duration(seconds: 5));

  final results = adapter.scanGetResults();
  var peripherals = 0;
  for (final device in results) {
    if (device.isConnectable) {
      print('Peripheral: ${device.identifier} [${device.address}]');
      peripherals++;
    }
  }

  print('Peripherals (accept connections): $peripherals');
  print('Broadcasters (advertise only): ${results.length - peripherals}');
}
program Roles;

{$IFDEF FPC}
  {$MODE DELPHIUNICODE}
{$ENDIF}
{$IFDEF MSWINDOWS}
  {$APPTYPE CONSOLE}
{$ENDIF}

uses
  {$IFDEF UNIX}
  cthreads,
  {$ENDIF}
  SimpleDelphiBLE;

var
  Adapter: TBleAdapter;
  Results: TArray<TBlePeripheral>;
  Device: TBlePeripheral;
  Peripherals: Integer;

begin
  Adapter := TBleAdapter.GetAdapters[0];
  Adapter.ScanFor(5000);

  Results := Adapter.ScanGetResults;
  Peripherals := 0;
  for Device in Results do
    if Device.IsConnectable then
    begin
      WriteLn('Peripheral: ', Device.Identifier, ' [', Device.Address, ']');
      Inc(Peripherals);
    end;

  WriteLn('Peripherals (accept connections): ', Peripherals);
  WriteLn('Broadcasters (advertise only): ', Length(Results) - Peripherals);
end.
import android.util.Log
import org.simpleble.android.Adapter

suspend fun countRoles() {
    val adapter = Adapter.getAdapters().first()
    adapter.scanFor(5000)

    val results = adapter.scanGetResults()
    var peripherals = 0
    for (device in results) {
        if (device.isConnectable) {
            Log.d("SimpleBLE", "Peripheral: ${device.identifier} [${device.address}]")
            peripherals++
        }
    }

    Log.d("SimpleBLE", "Peripherals (accept connections): $peripherals")
    Log.d("SimpleBLE", "Broadcasters (advertise only): ${results.size - peripherals}")
}

What you should see

The peripherals by name, then the two counts:

Peripheral: Kitchen Sensor [E4:7F:12:A0:6B:C2]
Peripheral: My Phone [5A:21:7C:90:3E:11]
Peripheral:  [71:E2:A9:AC:B5:E0]
Peripherals (accept connections): 3
Broadcasters (advertise only): 18

Most rooms hold more broadcasters than you expect. Phones, laptops, earbuds and trackers send short status advertisements all day, usually without a name. Some peripherals advertise without a name too, so their line shows only the address. On macOS the address is a UUID that macOS assigns to each device.

Exercise

Make your phone a peripheral. In nRF Connect for Mobile, open the Advertiser, add a connectable advertisement that includes the device name, and start it. Run the program again: the peripheral count goes up by one, and your phone's name appears in the list.

Troubleshooting

  • Both counts are zero. Bluetooth is off, or the program has no permission to scan. Run the program from What is Bluetooth Low Energy? first, then check Permissions.
  • Your phone doesn't appear. Keep the nRF Connect advertiser running and the phone unlocked, within a few meters of the computer.
  • React Native or Android finds nothing. The app needs its Bluetooth permissions before the scan. Android also delivers scan results only to apps that hold the location permission, unless the app declares that it never derives location from scans.

Lesson 1 introduces GATT, the data model a central and a peripheral share once connected, and Module 3 covers it in depth.

Next lesson

Scan for BLE devices: how scanning works, and how to find the closest device.

On this page