What This Post Covers
Before two Bluetooth Classic devices can exchange data, three things must happen in order: discovery (inquiry), connection (paging), and eventually disconnection. Each step maps to specific HCI commands and LMP messages exchanged between the host, local controller, and remote controller.
This post walks through all three procedures with exact message flow, HCI commands involved, and working BlueZ terminal commands and C code you can run on Linux.
Key Terms in This Post
Inquiry is how a Bluetooth device finds out who else is nearby. The device that starts the inquiry is called the inquirer. For a device to respond, it must first enable discoverable mode (inquiry scan ON). Once inquiry is running, the controller collects all responses and reports them to the host via Inquiry_Result events.
Figure 1 — Inquiry Procedure (HCI Message Flow)
| Host A | Controller A | Remote Device(s) |
| Remote enables Discoverable Mode: HCI_Write_Scan_Enable (Inquiry Scan ON) | ||
|
HCI_Inquiry |
||
|
Command_Status_Event |
||
|
Inquiry broadcast (air) |
||
|
Inquiry Response (air) |
||
| Each discoverable device returns: BD_ADDR + Class_Of_Device + Clock_Offset | ||
|
Inquiry_Result_Event(s) |
||
|
Inquiry_Complete_Event |
||
| HCI Command HCI EventOver-the-air (RF) | ||
Inquiry_Result_Event. When the inquiry window ends, Inquiry_Complete_Event is sent and the controller stops scanning.BlueZ — Run Inquiry from Terminal
# 1. Start Bluetooth service sudo systemctl start bluetooth # 2. Bring up the adapter sudo hciconfig hci0 up # 3. On the REMOTE device — enable discoverable mode sudo hciconfig hci0 piscan # 4. On the INITIATOR — run inquiry (~10 seconds) sudo hcitool inq # Sample output: # Inquiring ... # 00:1A:7D:DA:71:11 clock offset: 0x7e29 class: 0x5a020c # 00:1B:DC:0F:F5:41 clock offset: 0x6d15 class: 0x240414
BlueZ — Inquiry in C (HCI Socket)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
int main() {
int dev_id = hci_get_route(NULL); /* first available adapter */
int sock = hci_open_dev(dev_id);
if (dev_id < 0 || sock < 0) {
perror("Failed to open HCI device");
return 1;
}
int max_rsp = 255;
int len = 8; /* 8 x 1.28s = ~10 second inquiry */
int flags = IREQ_CACHE_FLUSH;
inquiry_info *devs = malloc(max_rsp * sizeof(inquiry_info));
/* Sends HCI_Inquiry → collects Inquiry_Result events */
int found = hci_inquiry(dev_id, len, max_rsp, NULL, &devs, flags);
char addr[19];
for (int i = 0; i < found; i++) {
ba2str(&(devs + i)->bdaddr, addr);
printf("[%d] BD_ADDR: %s\n", i + 1, addr);
}
free(devs);
close(sock);
return 0;
}
- Install:
sudo apt install libbluetooth-dev - Compile:
gcc inquiry.c -o inquiry -lbluetooth - Run:
sudo ./inquiry
Normal inquiry runs once. Periodic inquiry makes the controller repeat it automatically at a configurable interval — without the host needing to send a new command each cycle. You provide a minimum and maximum period; the controller picks a random time between them for each cycle. This randomisation prevents all nearby devices from scanning simultaneously.
Practical use: A kiosk detecting when phones enter or leave its range. Compare each new list with the previous — new entries = device arrived, missing entries = device left.
Figure 2 — Periodic Inquiry Timeline
| t=0 : HCI_Periodic_Inquiry_Mode sent | HCI_Exit_Periodic_Inquiry_Mode |
| Inquiry 1 | wait | Inquiry 2 | wait | Inquiry 3 | stopped |
Controller runs inquiries automatically. Host only stops it when needed.
BlueZ — Periodic Inquiry via Raw HCI
# Start periodic inquiry # OGF=0x01, OCF=0x0003 = HCI_Periodic_Inquiry_Mode # max_period=8 units, min_period=4 units, inq_len=4, num_rsp=0 (unlimited) sudo hcitool cmd 0x01 0x0003 0x08 0x00 0x04 0x00 0x33 0x8B 0x9E 0x04 0x00 # Stop periodic inquiry # OGF=0x01, OCF=0x0004 = HCI_Exit_Periodic_Inquiry_Mode sudo hcitool cmd 0x01 0x0004
In older Bluetooth, full discovery needed three separate exchanges: inquiry, then get name, then search services. With EIR (added in Bluetooth 2.1+EDR), the remote device includes its name, supported services, and RSSI right inside the inquiry response — doing everything in one shot.
Figure 3 — Standard Inquiry vs EIR
| ❌ Without EIR — 3 round-trips | ✅ With EIR — 1 round-trip |
|
1. Inquiry → BD_ADDR only
↓
2. HCI_Remote_Name_Request
↓
3. SDP Service Search
|
1. Inquiry → BD_ADDR + Name + Services + RSSI
Ready to connect!
No extra round-trips needed
|
BlueZ — Reading EIR Data
# EIR is transparent in bluetoothctl — device name shows in scan bluetoothctl [bluetooth]# scan on # [NEW] Device 00:1A:7D:DA:71:11 MyHeadphones <-- name from EIR # [NEW] Device 00:1B:DC:0F:F5:41 SomeSpeaker
/* Parse EIR fields from an inquiry_info_with_rssi record */
#include <stdint.h>
#include <stdio.h>
void parse_eir(uint8_t *eir, int eir_len) {
int pos = 0;
while (pos < eir_len) {
uint8_t field_len = eir[pos];
if (field_len == 0) break;
uint8_t type = eir[pos + 1]; /* EIR data type code */
uint8_t *data = &eir[pos + 2];
if (type == 0x09) /* 0x09 = Complete Local Name */
printf("Name: %.*s\n", field_len - 1, data);
if (type == 0x03) /* 0x03 = 16-bit UUIDs */
printf("Service UUID found\n");
pos += field_len + 1;
}
}
After inquiry, the next step is connecting. This procedure is called paging. The device that starts it is the initiator; the device being connected to is the target. The target must first enable connectable mode (page scan ON). Once connected, the initiator automatically becomes the Master of the piconet.
Figure 4 — Simplified Connection Establishment Flow
| Host A (Initiator) |
Controller A | Controller B | Host B (Target) |
| Host B: HCI_Write_Scan_Enable (Page Scan ON) → Controller B enters Connectable Mode | |||
|
HCI_Create_Connection |
|||
|
Command_Status_Event |
|||
|
LMP_host_connection_req |
|||
|
Connection_Request_Event |
|||
|
HCI_Accept_Conn_Request |
|||
| LMP_accepted + LMP_setup_complete ↔ LMP_setup_complete (over air between controllers) | |||
|
Connection_Complete ✓ |
Connection_Complete ✓ |
||
| Optional: Feature Exchange · AFH Enable · Authentication · Encryption | |||
| HCI Command HCI Event LMP Over-the-air ★ Initiator (A) becomes MASTER | |||
BlueZ — Create Connection from Terminal
# On the TARGET — enable connectable mode sudo hciconfig hci0 pscan # On the INITIATOR — connect to target sudo hcitool cc 00:1A:7D:DA:71:11 # Verify connection is up sudo hcitool con # Output: # Connections: # > ACL 00:1A:7D:DA:71:11 handle 1 state 1 lm MASTER
BlueZ — Create Connection in C
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int dev_id = hci_get_route(NULL);
int sock = hci_open_dev(dev_id);
bdaddr_t remote;
uint16_t handle;
str2ba("00:1A:7D:DA:71:11", &remote);
/*
* hci_create_connection() sends HCI_Create_Connection.
* The controller then sends LMP_host_connection_req over the air.
* Parameters: packet_type, clock_offset, role_switch, timeout_ms
*/
int ret = hci_create_connection(
sock, &remote,
htobs(0x0008), /* DM1 packet type */
htobs(0), /* clock offset (0 if unknown) */
0x00, /* role switch: 0 = don't allow */
&handle,
25000 /* timeout in ms */
);
if (ret < 0) perror("Connection failed");
else printf("Connected! Handle: %d\n", handle);
close(sock);
return 0;
}
- Install:
sudo apt install libbluetooth-dev - Compile:
gcc connect.c -o connect -lbluetooth - Run:
sudo ./connect
Either side can end an active connection at any time. The host sends HCI_Disconnect to its controller. The controller sends LMP_detach over the air to the remote. Both controllers then send Disconnection_Complete_Event to their respective hosts independently.
Figure 5 — Disconnection Procedure
| Host A | Controller A | Controller B | Host B |
| Active ACL connection exists. Either side may initiate disconnect. | |||
|
HCI_Disconnect |
|||
|
Command_Status_Event |
|||
|
LMP_detach (air) |
|||
|
ACK (air) |
|||
|
Disconnection_Complete ✓ |
Disconnection_Complete ✓ |
||
| HCI Command HCI EventLMP Over-the-air · Both hosts notified independently | |||
BlueZ — Disconnect from Terminal
# Disconnect by BD_ADDR sudo hcitool dc 00:1A:7D:DA:71:11 # Or with bluetoothctl bluetoothctl [bluetooth]# disconnect 00:1A:7D:DA:71:11 # [DEL] Device 00:1A:7D:DA:71:11 MyDevice
BlueZ — Disconnect in C
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int dev_id = hci_get_route(NULL);
int sock = hci_open_dev(dev_id);
/*
* Get handle first: sudo hcitool con
* "> ACL 00:1A:7D:DA:71:11 handle 1 ..."
*
* HCI_OE_USER_ENDED_CONNECTION = 0x13
* "Remote User Terminated Connection" — standard polite reason.
* hci_disconnect() sends HCI_Disconnect → controller sends LMP_detach.
*/
uint16_t handle = 1; /* replace with your actual handle */
int ret = hci_disconnect(sock, handle, HCI_OE_USER_ENDED_CONNECTION, 10000);
if (ret < 0) perror("Disconnect failed");
else printf("Disconnected cleanly.\n");
close(sock);
return 0;
}
- Get handle:
sudo hcitool con - Compile:
gcc disconnect.c -o disconnect -lbluetooth - Run:
sudo ./disconnect
| Procedure | HCI Command | LMP Message | HCI Event |
|---|---|---|---|
| Enable Discoverable | HCI_Write_Scan_Enable (Inq) |
— | Command_Complete |
| Inquiry | HCI_Inquiry |
Inquiry broadcast | Inquiry_Result, Inquiry_Complete |
| Periodic Inquiry | HCI_Periodic_Inquiry_Mode |
Inquiry broadcast (repeat) | Inquiry_Result (per cycle) |
| Enable Connectable | HCI_Write_Scan_Enable (Page) |
— | Command_Complete |
| Connect | HCI_Create_Connection |
LMP_host_connection_req | Connection_Complete |
| Accept Connection | HCI_Accept_Connection_Request |
LMP_accepted | Connection_Complete |
| Disconnect | HCI_Disconnect |
LMP_detach | Disconnection_Complete |
