Skip to content

Bluetooth Low Energy (BLE) — IoT Connectivity & Beacon Guide

DodaTech Updated 2026-06-24 6 min read

In this tutorial, you'll learn about Bluetooth Low Energy (BLE). We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Bluetooth Low Energy (BLE) is a wireless personal area network protocol designed for short-range communication with ultra-low power consumption, enabling IoT devices to operate for months or years on a single coin-cell battery.

Why BLE Matters

Classic Bluetooth consumes 1-100 watts and pairs devices for continuous streaming. BLE uses 0.01-0.5 watts and works on a broadcast-and-scan model. A BLE beacon broadcasting every second can run for two years on a CR2032 battery. Smartphones, laptops, and tablets all have BLE hardware built in, making it the most accessible IoT protocol for consumer-facing applications. Apple iBeacon and Google Eddystone use BLE advertising for proximity detection. Medical devices use BLE for continuous glucose monitoring and pulse oximeters. Fitness trackers sync step data over BLE. Doda Browser uses BLE Web API to scan for nearby beacons and deliver location-aware content.

Plain-Language Explanation

Think of BLE like a bulletin board in a hallway. A beacon (like a store) pins a note to the board saying "Store A is 5 meters away with a sale on shoes." Your phone walks past, reads the note, and shows you a notification. No pairing needed — the phone just scans the board periodically. This is BLE advertising mode.

For two-way communication, BLE uses the GATT (Generic Attribute Profile) model. One device acts as a GATT server (has data), the other as a GATT client (reads data). The server organizes data into services (like "Battery Service") containing characteristics (like "Battery Level"). The client reads or subscribes to characteristic updates.

graph TD
    subgraph "BLE Communication Modes"
        B[Beacon] -->|Advertising
iBeacon / Eddystone| S[Smartphone] S -->|Scan request| B B -->|Scan response| S end subgraph "GATT Client-Server" Server[BLE Peripheral
Sensor Tag] -->|Service: Temperature| Char1[Characteristic: Temperature] Server -->|Service: Battery| Char2[Characteristic: Battery Level] Client[BLE Central
Smartphone App] -->|Read| Char1 Client -->|Subscribe| Char2 end style B fill:#3498db,color:#fff style Server fill:#27ae60,color:#fff style Client fill:#e67e22,color:#fff

BLE Roles

Broadcaster: Transmits advertising packets. Beacons are broadcasters. They have no connections.

Observer: Scans for advertising packets without connecting. A smartphone scanning for beacons is an Observer.

Peripheral (Server): Advertises its presence and accepts connections. A temperature sensor acting as a GATT server is a peripheral.

Central (Client): Scans for peripherals and initiates connections. A smartphone app is a central.

Advertising Protocol

BLE advertising uses three dedicated channels (37, 38, 39) spread across the 2.4 GHz band to avoid WiFi interference. Each advertising channel carries the same packet to increase reliability.

An advertising packet contains:

  • PDU type (connectable, non-connectable, scan request)
  • Advertiser address (public or random)
  • Advertising data (31 bytes) — manufacturer data, service UUIDs, device name
  • Scan response data (31 bytes) — additional data the device can provide on request

GATT Services and Characteristics

Standard services are defined by the Bluetooth SIG. Common ones:

Service UUID Characteristics
Battery Service 0x180F Battery Level
Device Information 0x180A Manufacturer Name, Model Number, Serial Number
Environmental Sensing 0x181A Temperature, Humidity, Pressure
Heart Rate 0x180D Heart Rate Measurement, Body Sensor Location

Arduino BLE Peripheral Example

#include <BLEPeripheral.h>

BLEPeripheral blePeripheral;
BLEService envService("181A");  // Environmental Sensing Service
BLEFloatCharacteristic tempChar("2A6E", BLERead | BLENotify);
BLEFloatCharacteristic humChar("2A6F", BLERead | BLENotify);

void setup() {
  Serial.begin(115200);

  blePeripheral.setLocalName("DodaEnvSensor");
  blePeripheral.setAdvertisedServiceUuid(envService.uuid());

  envService.addCharacteristic(tempChar);
  envService.addCharacteristic(humChar);
  blePeripheral.addAttribute(envService);

  blePeripheral.begin();
  Serial.println("BLE Environmental Sensor Ready");
}

void loop() {
  blePeripheral.poll();

  float temperature = 23.5;
  float humidity = 55.2;

  tempChar.setValue(temperature);
  humChar.setValue(humidity);

  delay(10000);  // Update every 10 seconds
}

Python BLE Scanner

import asyncio
from bleak import BleakScanner

async def scan_devices():
    devices = await BleakScanner.discover(timeout=5.0)
    for d in devices:
        print(f"Device: {d.name or 'Unnamed'}")
        print(f"  Address: {d.address}")
        print(f"  RSSI: {d.rssi} dBm")
        print(f"  Metadata: {d.metadata}")
        print()

devices = asyncio.run(scan_devices())

Expected output:

Device: DodaEnvSensor
  Address: C4:5B:BE:01:23:45
  RSSI: -67 dBm
  Metadata: {'uuids': ['181A'], 'manufacturer_data': {0xFFFF: b'\x01'}}

Device: Unnamed
  Address: D3:21:4A:67:89:AB
  RSSI: -85 dBm
  Metadata: {'uuids': [], 'manufacturer_data': {}}

BLE Beacons

Beacons are non-connectable BLE devices that broadcast a fixed payload. Two major formats:

iBeacon (Apple): Proximity UUID (16 bytes), Major (2 bytes), Minor (2 bytes), TX power. Used for indoor navigation and proximity marketing.

Eddystone (Google): Multiple frame types — UID (unique beacon ID), URL (direct eddystone:// URL), TLM (telemetry — battery, temperature). Eddystone-URL is clever: the beacon broadcasts a compressed URL that any smartphone can resolve.

Python iBeacon Decoder

import asyncio
from bleak import BleakScanner

def beacon_callback(device, advertising_data):
    if advertising_data.manufacturer_data:
        for manufacturer_id, data in advertising_data.manufacturer_data.items():
            # Apple manufacturer ID is 0x004C
            if manufacturer_id == 0x004C and len(data) >= 25:
                # iBeacon data starts with 0x02 0x15
                if data[0] == 0x02 and data[1] == 0x21:
                    uuid = data[2:18].hex()
                    major = int.from_bytes(data[18:20], 'big')
                    minor = int.from_bytes(data[20:22], 'big')
                    tx_power = data[22] - 256
                    distance = 10 ** ((-69 - tx_power) / (10 * 2.0))
                    print(f"iBeacon: {uuid} Major:{major} Minor:{minor} "
                          f"TX:{tx_power} dBm ~{distance:.1f}m")

async def main():
    scanner = BleakScanner(beacon_callback)
    await scanner.start()
    await asyncio.sleep(10.0)
    await scanner.stop()

asyncio.run(main())

Common Mistakes

  1. Not managing connection intervals: BLE devices negotiate connection intervals. A short interval (7.5 ms) drains battery. Set an interval appropriate for your data rate (100 ms for sensors).

  2. Large advertising payloads: Advertising data is limited to 31 bytes. Use scan response for additional data. Keep the primary advertising payload minimal.

  3. Ignoring BLE coexistence: BLE, WiFi, and Zigbee all share 2.4 GHz. In dense deployments, adaptive frequency hopping helps, but congested environments cause packet loss.

  4. No power optimization: Notifying characteristics every 100 ms when data changes once per minute wastes battery. Only send updates when values actually change.

  5. Forgetting security: BLE pairing can use "Just Works" (no security), passkey entry, or numeric comparison. For sensitive data like medical readings, require authenticated pairing with encryption.

Practice Questions

  1. What is the difference between BLE advertising and GATT? Advertising is a connectionless broadcast model (one-to-many). GATT is a connection-oriented client-server model (one-to-one) for bidirectional data exchange.

  2. How does BLE achieve lower power than classic Bluetooth? BLE uses shorter radio bursts, longer sleep intervals, simpler protocol stack, and connectionless advertising modes. Peak current is ~15 mA vs ~30 mA for classic BT.

  3. What is the purpose of BLE connection intervals? Connection intervals define how often the peripheral and central exchange data between connection events. Longer intervals reduce power consumption but increase latency.

  4. Why do beacons use non-connectable advertising? Beacons broadcast to any scanning device without establishing a connection. This allows one-to-many broadcasting with the lowest possible power consumption.

  5. What information does an iBeacon advertising packet contain? iBeacon packets contain a proximity UUID, major number, minor number, and calibrated TX power level. The receiving device calculates distance from RSSI and TX power.

Mini Project

Build a BLE environmental sensor simulator:

import asyncio
import random
from bleak import BleakClient

SENSOR_SERVICE_UUID = "181A"
TEMP_CHAR_UUID = "2A6E"
HUM_CHAR_UUID = "2A6F"

class BLEVirtualSensor:
    def __init__(self):
        self.connected = False

    async def connect_and_read(self, address: str):
        async with BleakClient(address) as client:
            self.connected = await client.is_connected()
            print(f"Connected: {self.connected}")

            while True:
                temp_bytes = await client.read_gatt_char(TEMP_CHAR_UUID)
                hum_bytes = await client.read_gatt_char(HUM_CHAR_UUID)

                temperature = float(temp_bytes.decode())
                humidity = float(hum_bytes.decode())

                print(f"Temperature: {temperature}°C, Humidity: {humidity}%")
                await asyncio.sleep(5)

# Run with a real BLE device address
# asyncio.run(BLEVirtualSensor().connect_and_read("C4:5B:BE:01:23:45"))

Cross-References

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro