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, the local controller, and the remote controller.
This post walks through all three procedures — showing the exact message flow, the 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 initiates the inquiry is called the inquirer. For a device to be found, it must first be set to discoverable mode — meaning its inquiry scan is enabled.
The host enables discoverable mode by sending HCI_Write_Scan_Enable with the Inquiry Scan bit set. After that the device starts listening for inquiry packets from other devices.
Figure 1 — Inquiry Procedure (HCI Message Flow)
Host A Controller A Remote Device(s) Remote device: HCI_Write_Scan_Enable (Inquiry Scan ON) → enters Discoverable Mode 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 CommandHCI EventOver-the-air (RF)
Inquiry_Result_Event. When the inquiry period ends, the controller sends Inquiry_Complete_Event and stops listening.BlueZ — Run Inquiry from Terminal
# 1. Start the Bluetooth service sudo systemctl start bluetooth # 2. Bring up the adapter sudo hciconfig hci0 up # 3. Make the remote device discoverable (run on the OTHER machine) sudo hciconfig hci0 piscan # 4. Run inquiry — scan for ~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)
This is exactly what hcitool inq does internally. The library call hci_inquiry() sends HCI_Inquiry to the controller and collects the Inquiry_Result events for you.
#include <stdio.h>
#include <stdlib.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));
/* hci_inquiry() sends HCI_Inquiry and waits for 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 dev library:
sudo apt install libbluetooth-dev - Compile:
gcc inquiry.c -o inquiry -lbluetooth - Run:
sudo ./inquiry
Normal inquiry runs once. Periodic inquiry tells the controller to repeat it automatically at a set interval — without the host needing to send a new command each time. You provide a minimum and maximum period; the controller picks a random time between them for each cycle.
A practical use: a kiosk that wants to detect when a phone enters or leaves Bluetooth range. Compare each new device list with the previous one — new entries mean a device arrived, missing entries mean it 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 Command
# Start periodic inquiry # Opcode: OGF=0x01, OCF=0x0003 = HCI_Periodic_Inquiry_Mode # max_period=8 units (~10s), min_period=4 units (~5s), inquiry_length=4 units sudo hcitool cmd 0x01 0x0003 0x08 0x00 0x04 0x00 0x33 0x8B 0x9E 0x04 0x00 # Stop periodic inquiry # Opcode: OGF=0x01, OCF=0x0004 = HCI_Exit_Periodic_Inquiry_Mode sudo hcitool cmd 0x01 0x0004
In older Bluetooth versions, a full discovery took three separate exchanges: inquiry → get name → search services. With EIR (introduced in Bluetooth 2.1+EDR), the remote device can include its name, supported services, and RSSI right inside the inquiry response — reducing setup time significantly.
Figure 3 — Standard Inquiry vs EIR
Without EIR — 3 round-trips 1. Inquiry → BD_ADDR only 2. HCI_Remote_Name_Request 3. SDP Service Search With EIR — 1 round-trip 1. Inquiry → BD_ADDR + Name + Services + RSSI ✅ Ready to connect! No extra round-trips needed
BlueZ — Reading EIR Data
# bluetoothctl shows device name directly — that is EIR working bluetoothctl [bluetooth]# scan on # Output: # [NEW] Device 00:1A:7D:DA:71:11 MyHeadphones <-- name came from EIR # [NEW] Device 00:1B:DC:0F:F5:41 SomeSpeaker
/* Parse EIR data from an inquiry result 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 */
uint8_t *data = &eir[pos + 2];
if (type == 0x09) { /* 0x09 = Complete Local Name */
printf("Device name: %.*s\n", field_len - 1, data);
}
if (type == 0x03) { /* 0x03 = 16-bit Service UUIDs */
printf("Service UUID present\n");
}
pos += field_len + 1;
}
}
After finding a device through inquiry, the next step is connecting to it. This procedure is called paging. The device that starts the connection is the initiator; the device being connected to is the target. The target must be in connectable mode (page scan enabled) for the connection to succeed. Once connected, the initiator becomes the Master of the piconet.
Figure 4 — Simplified Connection Establishment Flow
Host A (Initiator) Controller A (Local) Controller B (Remote) 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 next steps: Feature Exchange · AFH Enable · Authentication · Encryption HCI CommandHCI EventLMP Over-the-airInitiator (A) becomes MASTER after connection
BlueZ — Create Connection from Terminal
# Make the target connectable first (run on target machine) sudo hciconfig hci0 pscan # Connect to the target device (run on initiator) 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 to the
* controller. The controller then sends LMP_host_connection_req
* to the remote device over the air.
*
* Parameters:
* pkt_type = 0x0008 (DM1 ACL packet)
* clock_off = 0 (use 0 if not known from inquiry)
* role_switch= 0 (do not allow role switch)
* timeout = 25000ms
*/
int ret = hci_create_connection(
sock, &remote,
htobs(0x0008), /* packet type */
htobs(0), /* clock offset */
0x00, /* role switch */
&handle,
25000
);
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 - Make sure target is in connectable mode:
sudo hciconfig hciX pscan - 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 controller. Both controllers then fire a Disconnection_Complete_Event up to their respective hosts independently.
Figure 5 — Disconnection Procedure
Host A Controller A Controller B Host B Active ACL connection exists between Device A and Device B. Either side can disconnect. HCI_Disconnect Command_Status_Event LMP_detach (air) ACK (air) Disconnection_Complete ✓ Disconnection_Complete ✓ HCI CommandHCI EventLMP Over-the-airBoth hosts notified independently
BlueZ — Disconnect from Terminal
# Disconnect by BD_ADDR sudo hcitool dc 00:1A:7D:DA:71:11 # Or using bluetoothctl (interactive) 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 the connection handle from: sudo hcitool con
* Look for: "> ACL 00:1A:7D:DA:71:11 handle 1 ..."
*
* Reason 0x13 = "Remote User Terminated Connection"
* This is the standard polite disconnect reason code.
* hci_disconnect() sends HCI_Disconnect to the controller.
* The controller then sends LMP_detach over the air.
*/
uint16_t handle = 1; /* replace with 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— note the handle number next to the BD_ADDR - Compile:
gcc disconnect.c -o disconnect -lbluetooth - Run:
sudo ./disconnect
| Procedure | HCI Command | LMP Message | HCI Event |
|---|---|---|---|
| Enable Discoverable | HCI_Write_Scan_Enable (Inquiry) |
— | Command_Complete |
| Inquiry | HCI_Inquiry |
Inquiry broadcast (RF) | 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 (Initiator) | 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 |
Continue Learning Bluetooth
Explore the full Bluetooth stack — Radio to GATT — with free courses on EmbeddedPathashala
