Mastering BLE on Mobile: From Basics to Advanced Data Handling
This article explains Bluetooth Low Energy fundamentals, compares classic and BLE, describes BLE mesh networking, provides a complete bleno example for peripheral simulation, and details advertising, scanning, connection flow, MTU negotiation, fragmentation, and sticky‑packet handling on Android and iOS.
Background
Bluetooth was first released in 1998, but its true IoT adoption began with the fourth‑generation Bluetooth 4.0+ BLE standard in 2010. iPhone 4S was the first smartphone supporting Bluetooth 4.0, and Android 4.3 (API 18) introduced a BLE API for developers. BLE is widely used in wearables, smart scales, door locks, etc., while classic Bluetooth is used for audio streaming. This article focuses on BLE characteristics and mobile‑side development considerations.
What Is Low‑Power Bluetooth?
BLE consumes far less power and cost than classic Bluetooth, making it ideal for IoT devices. Classic Bluetooth offers higher data rates (up to 3 Mbps) and requires pairing, whereas BLE can operate without pairing and typically transmits around 1 Mbps in intermittent bursts.
Bluetooth Mesh Network
A Bluetooth mesh (micro‑network) consists of one master device and up to seven slaves. Any device can act as master or slave, but a device cannot be both simultaneously. The master synchronises clocks and initiates connections; slaves respond to the master’s commands.
Communication between master and slaves follows a client‑server model: the master can poll slaves or receive notifications from them.
Example with bleno
The bleno library can simulate a BLE peripheral on Windows, Linux, or macOS. The following code creates a broadcast name "bill", defines a service UUID, a characteristic that supports read and write, and starts advertising when the Bluetooth adapter is powered on.
const name = 'bill'; // broadcast device name
const serviceUuids = ['fffffffffffffffffffffffffffffff0']; // broadcast service UUID
bleno.on('stateChange', (state) => {
if (state === 'poweredOn') {
bleno.startAdvertising(name, serviceUuids);
const characteristic = new Characteristic({
uuid: 'fffffffffffffffffffffffffffffff2',
properties: ['read', 'write'],
descriptors: [
new Descriptor({ uuid: '2901', value: 'This is a test characteristic' })
]
});
const service = new PrimaryService({
uuid: 'fffffffffffffffffffffffffffffff1',
characteristics: [characteristic]
});
bleno.setServices([service]);
}
});To add read/write handling:
bleno.on('stateChange', (state) => {
if (state === 'poweredOn') {
bleno.startAdvertising(name, serviceUuids);
const characteristic = new Characteristic({
uuid: 'fffffffffffffffffffffffffffffff2',
properties: ['read', 'write'],
descriptors: [
new Descriptor({ uuid: '2901', value: 'This is a test characteristic' })
],
onWriteRequest: (data, offset, withoutResponse, callback) => {
console.log('onWriteRequest:', data.toString('hex'));
callback(Characteristic.RESULT_SUCCESS);
},
onReadRequest: (offset, callback) => {
callback(Characteristic.RESULT_SUCCESS, Buffer.from('Hello, bill!', 'utf8'));
}
});
const service = new PrimaryService({
uuid: 'fffffffffffffffffffffffffffffff1',
characteristics: [characteristic]
});
bleno.setServices([service]);
}
});Bluetooth Connection
Advertising
The peripheral must be in a "discoverable" state, broadcasting packets that contain device features such as the name. Each advertising packet is limited to 31 bytes and is transmitted on three advertising channels.
Scanning
The scanner (e.g., a phone) receives these packets and can initiate a connection when it finds a matching device. Android requires location services to be enabled for BLE scanning.
Connection Process
After a successful connection, the devices negotiate MTU, discover services and characteristics, and the central (master) subscribes to characteristic changes. The peripheral stops advertising once connected.
Data Packets and MTU
After connection, the devices negotiate the Maximum Transmission Unit (MTU). BLE 4.2 and later support up to 244 bytes per packet, while earlier versions cap MTU at 23 bytes (20 bytes payload after ATT header).
iOS devices (e.g., iPhone 14) automatically negotiate a large MTU (up to 512 bytes), whereas Android often defaults to 23 bytes unless the app explicitly requests a larger MTU. Some mini‑program platforms (e.g., DingTalk) do not expose MTU APIs, forcing developers to rely on the underlying BLE fragmentation.
Fragmentation and Sticky Packets
If a payload exceeds the negotiated MTU, the BLE stack splits it into multiple packets. On Android, a 27‑byte payload (e.g., "123456789126456789123456789") is fragmented into two packets, causing a time gap between arrivals. This can lead to "sticky packet" problems where multiple frames merge.
Typical solutions include:
Using a fixed header or footer to delimit frames.
Defining a constant message length.
Reading a length field defined by the protocol.
Summary
Android requires location permission for BLE scanning, and its default MTU payload is 20 bytes; therefore, protocols should support at least 20‑byte packets for maximum compatibility. iOS automatically negotiates a larger MTU. When developing for platforms that lack MTU APIs (e.g., DingTalk mini‑programs), developers must handle BLE’s built‑in fragmentation and implement sticky‑packet mitigation strategies.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
