What This Post Covers
Hello students welcome to embeddedpathashalas free embedded systems course, this post about Bluetooth HFP tutorial BlueZ Programming is part of our bluetooth development course, This post covers two Bluetooth Classic profiles that every embedded engineer working with hands-free audio or file transfer should know: the Hands-Free Profile (HFP) and the Generic Object Exchange Profile (GOEP).
HFP is the profile that makes your car’s Bluetooth system work with your phone. GOEP is the base profile that enables file and object transfer on Bluetooth — it underpins the File Transfer Profile (FTP) and Object Push Profile (OPP). Both profiles are built on top of RFCOMM, L2CAP, and use BlueZ on Linux. Every section includes working BlueZ code or terminal commands you can run on Ubuntu.
Key Terms in This Post
Both HSP and HFP let you make and receive audio calls wirelessly. But HFP was designed for richer scenarios — primarily in-car systems — where the headset needs to do more than just carry audio. HFP adds control features that HSP simply does not have.
HSP vs HFP — Feature Comparison
| Feature | HSP | HFP |
|---|---|---|
| Audio call (voice) | ✅ | ✅ |
| Answer / reject call | ✅ basic | ✅ full |
| Dial a number from headset | ❌ | ✅ |
| Redial last number | ❌ | ✅ |
| Voice recognition activation | ❌ | ✅ |
| Call waiting notification | ❌ | ✅ |
| Three-way calling | ❌ | ✅ |
| Caller ID (CLI) | ❌ | ✅ |
| Phone signal / battery on headset | ❌ | ✅ |
| Echo cancellation / noise reduction | ❌ | ✅ |
| Remote audio volume control | ❌ | ✅ |
HFP defines exactly two roles. Every device in an HFP session must be one of these:
HFP Roles Explained
| 📱 Audio Gateway (AG) | 🎧 Hands-Free Unit (HF) |
|
What it is: The device that connects to the actual audio network — the cellular network or internet calls. It is the gateway through which audio enters and leaves the Bluetooth system. Typical devices: Smartphone Tablet with SIM
Responsibilities:
|
What it is: The device that handles audio input (microphone) and output (speaker) on behalf of the user. It also provides the interface to control calls via AT commands sent to the AG. Typical devices: Car kit Wireless headset Embedded handset
Responsibilities:
|
HFP uses two completely separate connections simultaneously — one for control, one for audio. Getting this right is the key to understanding HFP architecture.
Figure 1 — HFP Two-Connection Architecture
| Connection 1: Service Level | Connection 2: Audio |
|
Protocol: RFCOMM channel (over L2CAP over ACL) Purpose: Carry AT command text back and forth between AG and HF Always active when devices are connected — stays open even when no call is in progress Examples of what travels here:
|
Protocol: SCO or eSCO link (separate from ACL) Purpose: Carry real-time voice audio between AG and HF Set up only when a call starts — torn down when call ends eSCO vs SCO:
|
Figure 2 — HFP Protocol Stack (AG Side and HF Side)
| 📱 Audio Gateway (Phone) | 🎧 Hands-Free Unit (Car kit) | ||
|
Application
(Audio port emulation · AT cmd handler) |
Application
(Audio driver · Speaker/Mic relay) |
||
|
SDP
|
SDP
|
||
|
RFCOMM (AT commands travel here)
|
RFCOMM (AT commands travel here)
|
||
|
L2CAP
|
L2CAP
|
||
|
Lower Layers (LMP · Baseband · Radio)
|
Lower Layers (LMP · Baseband · Radio)
|
||
|
|||
All control between the AG and HF happens through AT commands sent over the RFCOMM service level connection. The HF sends commands; the AG sends responses and unsolicited result codes (notifications). The set of AT commands used by HFP is defined in 3GPP TS 27.007.
Figure 3 — AT Command Flow: Answering an Incoming Call
| 📱 Audio Gateway (Phone) | 🎧 Hands-Free Unit |
| Phone receives an incoming call from cellular network | |
|
RING |
|
|
+CLIP: “9876543210”,129 |
|
| HF shows caller number on display | |
|
ATA |
|
|
OK |
|
| ✅ AG sets up SCO/eSCO audio link — voice call begins | |
Key HFP AT Commands Reference
| AT Command | Sender | Purpose |
|---|---|---|
| ATA | HF → AG | Answer incoming call |
| ATD<number>; | HF → AG | Dial a number |
| AT+BLDN | HF → AG | Redial last number |
| AT+CHUP | HF → AG | Hang up / reject call |
| AT+CIND? | HF → AG | Query call indicators (signal, battery, call status) |
| AT+CLCC | HF → AG | List all current calls with status |
| AT+CHLD=<n> | HF → AG | Call hold / three-way call handling |
| AT+BVRA=1 | HF → AG | Activate voice recognition on AG |
| +RING | AG → HF | Unsolicited: incoming call ringing |
| +CLIP: “num”,type | AG → HF | Caller ID push to HF display |
| +CIEV: ind,val | AG → HF | Indicator event: signal=4, battery=3, call=1, etc. |
| +CCWA: “num”,type | AG → HF | Call waiting notification while on another call |
On Linux, HFP is handled by oFono (Open Source Telephony for Linux) working alongside BlueZ. oFono registers itself as the HFP Audio Gateway handler through BlueZ’s D-Bus API. BlueZ manages the Bluetooth connection; oFono handles all the AT command processing and audio routing.
Setup oFono + BlueZ HFP on Ubuntu
# Install oFono sudo apt install ofono # Start oFono daemon sudo systemctl start ofono # Start BlueZ sudo systemctl start bluetooth # Pair your HFP device (phone or headset) bluetoothctl [bluetooth]# scan on [bluetooth]# pair 00:1A:7D:DA:71:11 [bluetooth]# connect 00:1A:7D:DA:71:11 [bluetooth]# trust 00:1A:7D:DA:71:11 # Check oFono sees the HFP modem dbus-send --system --print-reply \ --dest=org.ofono / org.ofono.Manager.GetModems
Open a Raw RFCOMM HFP Channel in C
If you want to talk AT commands directly — useful when writing your own HFP Audio Gateway or HF unit — you open an RFCOMM socket and write AT command strings to it. The RFCOMM channel number for HFP is found through SDP service discovery.
/*
* hfp_at_raw.c — Open RFCOMM channel to HFP device and exchange AT commands
* Compile: gcc hfp_at_raw.c -o hfp_at_raw -lbluetooth
* Run: sudo ./hfp_at_raw
*
* NOTE: rfcomm_channel should be discovered via SDP first.
* For quick testing, HFP typically uses channel 1 or 3 — check with:
* sdptool browse 00:1A:7D:DA:71:11
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>
#define REMOTE_ADDR "00:1A:7D:DA:71:11" /* replace with your device */
#define RFCOMM_CHANNEL 1 /* HFP channel from SDP */
static int send_at(int fd, const char *cmd) {
char buf[256];
/* AT commands in HFP end with carriage return */
int n = snprintf(buf, sizeof(buf), "%s\r", cmd);
write(fd, buf, n);
printf("Sent: %s\n", cmd);
/* Read response — simple blocking read for demo */
memset(buf, 0, sizeof(buf));
int len = read(fd, buf, sizeof(buf) - 1);
if (len > 0) printf("Received: %s\n", buf);
return len;
}
int main() {
struct sockaddr_rc addr = {0};
int sock;
sock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
addr.rc_family = AF_BLUETOOTH;
addr.rc_channel = RFCOMM_CHANNEL;
str2ba(REMOTE_ADDR, &addr.rc_bdaddr);
printf("Connecting to %s channel %d ...\n", REMOTE_ADDR, RFCOMM_CHANNEL);
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect failed");
return 1;
}
printf("Connected. Starting HFP handshake.\n\n");
/* HFP connection setup sequence (HF side) */
send_at(sock, "AT+BRSF=20"); /* HF supported features bitmask */
send_at(sock, "AT+CIND=?"); /* query supported indicators */
send_at(sock, "AT+CIND?"); /* get current indicator values */
send_at(sock, "AT+CMER=3,0,0,1");/* enable indicator reporting */
send_at(sock, "AT+CHLD=?"); /* query 3-way call support */
printf("\nHFP Service Level Connection established.\n");
printf("Now waiting for calls — press Ctrl+C to exit.\n");
/* Read unsolicited AT notifications in a loop */
char buf[256];
while (1) {
memset(buf, 0, sizeof(buf));
int n = read(sock, buf, sizeof(buf) - 1);
if (n <= 0) break;
printf("AG says: %s", buf);
/* If AG rings, automatically answer */
if (strstr(buf, "RING")) {
sleep(2);
send_at(sock, "ATA"); /* answer the call */
}
}
close(sock);
return 0;
}
Find HFP RFCOMM Channel via SDP
# Find the RFCOMM channel number registered for HFP service on a device sdptool browse 00:1A:7D:DA:71:11 # Look for output like: # Service Name: Handsfree # Protocol Descriptor List: # "L2CAP" (0x0100) # "RFCOMM" (0x0003) # Channel: 3 <-- this is the channel number to use # You can also filter directly: sdptool search --bdaddr 00:1A:7D:DA:71:11 HANDSFREE
BTPROTO_SCO. oFono handles this through the BlueZ audio API in production systems.
GOEP is the base profile that defines how two Bluetooth devices exchange objects — files, contacts, calendar entries, notes, or any binary data. It is not a complete application by itself; instead it is the foundation that other profiles like OPP (Object Push Profile) and FTP (File Transfer Profile) are built on top of.
The protocol GOEP uses for actual data exchange is OBEX (Object Exchange), which runs over RFCOMM, which runs over L2CAP. OBEX itself was borrowed from IrDA (infrared) — the same protocol that older Palm devices used to beam contacts to each other.
GOEP Roles — Server and Client
| 📁 GOEP Server | 💻 GOEP Client |
|
|
Figure 4 — GOEP Protocol Stack
| Client Application | Server Application |
|
OBEX (Adopted from IrOBEX)
|
OBEX (Adopted from IrOBEX)
|
|
SDP
RFCOMM
|
SDP
RFCOMM
|
|
L2CAP
|
L2CAP
|
|
Lower Layers (LMP · Baseband · Radio)
|
|
|
Profiles built on GOEP: OPP — Object Push FTP — File Transfer PBAP — Phonebook MAP — Message Access
|
|
OBEX uses a request-response model similar to HTTP. The client sends a request (CONNECT, PUT, GET, SETPATH, DISCONNECT) and the server responds with a status code (Success, Continue, Unauthorized, Not Found, etc.).
Figure 5 — OBEX File Transfer Flow (Client pushes a file to Server)
| 💻 OBEX Client | 📁 OBEX Server |
|
OBEX CONNECT (version, max packet size) |
|
|
200 OK (Connection ID) |
|
|
PUT (Name: “photo.jpg”, Length: 500KB, Body chunk 1) |
|
|
100 CONTINUE (send more) |
|
| … more PUT chunks until End-of-Body … | |
|
200 OK (file received) |
|
|
OBEX DISCONNECT |
|
| OBEX session closed · RFCOMM channel released | |
On Linux, OBEX/GOEP is handled by obexd — a standalone daemon that ships with BlueZ. It exposes the org.bluez.obex D-Bus interface for both client and server operations.
Push a File Using obexctl (OPP)
# Start obexd if not already running sudo systemctl start obex # or: /usr/lib/bluetooth/obexd -n & # Use obexctl to push a file to a remote device obexctl [obex]# connect 00:1A:7D:DA:71:11 [00:1A:7D:DA:71:11]# send /home/user/photo.jpg [00:1A:7D:DA:71:11]# disconnect # Or send a vCard (contact) via OPP obexctl [obex]# connect 00:1A:7D:DA:71:11 [00:1A:7D:DA:71:11]# send /home/user/contact.vcf
Push a File via D-Bus in C
/*
* obex_push.c — Push a file to a remote device using obexd D-Bus API
* Compile: gcc obex_push.c -o obex_push `pkg-config --cflags --libs gio-2.0`
* Run: ./obex_push 00:1A:7D:DA:71:11 /home/user/photo.jpg
*
* This calls org.bluez.obex.Client1.SendFile() which is the standard
* OPP (Object Push Profile) push operation.
*/
#include <gio/gio.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <bdaddr> <filepath>\n", argv[0]);
return 1;
}
const char *bdaddr = argv[1]; /* e.g. "00:1A:7D:DA:71:11" */
const char *filepath = argv[2]; /* e.g. "/home/user/photo.jpg" */
GError *error = NULL;
GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (!conn) {
fprintf(stderr, "D-Bus connection failed: %s\n", error->message);
return 1;
}
/* Call org.bluez.obex.Client1.CreateSession to open OBEX session */
GVariantBuilder *b = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(b, "{sv}", "Target",
g_variant_new_string("opp")); /* OPP profile */
GVariant *session_result = g_dbus_connection_call_sync(
conn,
"org.bluez.obex", /* D-Bus name of obexd */
"/org/bluez/obex", /* object path */
"org.bluez.obex.Client1", /* interface */
"CreateSession", /* method */
g_variant_new("(sa{sv})", bdaddr, b),
G_VARIANT_TYPE("(o)"),
G_DBUS_CALL_FLAGS_NONE,
-1, NULL, &error
);
if (!session_result) {
fprintf(stderr, "CreateSession failed: %s\n", error->message);
return 1;
}
const char *session_path;
g_variant_get(session_result, "(&o)", &session_path);
printf("OBEX session opened: %s\n", session_path);
/* Call org.bluez.obex.ObjectPush1.SendFile on the session */
GVariant *push_result = g_dbus_connection_call_sync(
conn,
"org.bluez.obex",
session_path,
"org.bluez.obex.ObjectPush1", /* OPP interface */
"SendFile", /* push the file */
g_variant_new("(s)", filepath),
G_VARIANT_TYPE("(o)"),
G_DBUS_CALL_FLAGS_NONE,
-1, NULL, &error
);
if (!push_result) {
fprintf(stderr, "SendFile failed: %s\n", error->message);
} else {
printf("File push started: %s\n", filepath);
g_variant_unref(push_result);
}
g_variant_unref(session_result);
g_variant_builder_unref(b);
g_object_unref(conn);
return 0;
}
Receive Files — Start OBEX Server
# Start obexd in server mode — auto-accept incoming file pushes # Files are saved to /tmp/obex by default /usr/lib/bluetooth/obexd --auto-accept --root /tmp/obex & # Or for interactive accept/reject, just start obexd normally # and use bluetoothctl / obexctl to manage incoming transfers # Check what the OBEX server SDP record looks like: sdptool browse local | grep -A 10 "OBEX Object Push"
| Item | Profile | Details |
|---|---|---|
| Control channel | HFP | RFCOMM → L2CAP → ACL. Carries AT commands. Always active. |
| Audio channel | HFP | SCO or eSCO link. Set up only when a call is active. |
| Answer call | HFP | ATA |
| Dial number | HFP | ATD<number>; |
| Hang up | HFP | AT+CHUP |
| Linux HFP daemon | HFP | oFono + BlueZ working together via D-Bus |
| GOEP transport | GOEP | OBEX → RFCOMM → L2CAP → ACL |
| Push a file | GOEP/OPP | obexctl → send <file> |
| Linux OBEX daemon | GOEP | obexd — exposes org.bluez.obex D-Bus API |
| Profiles using GOEP | GOEP | OPP, FTP, PBAP (phonebook), MAP (messages) |
Next Up: A2DP & AVRCP — Bluetooth Audio Streaming
The next post covers A2DP audio streaming and AVRCP remote control — including AVDTP signalling, codec negotiation, and BlueZ PulseAudio/PipeWire integration on Linux. I hope you have leart basics about Bluetooth HFP tutorial BlueZ Programming
