Bluetooth HFP tutorial BlueZ Programming

Bluetooth HFP tutorial BlueZ Programming
Hands-Free Profile · Audio Gateway · AT Commands · GOEP · OBEX · Full BlueZ C Code on Linux
🎧
HFP
📱
Audio Gateway
📡
AT Commands
📂
GOEP / OBEX

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.

💡 Prerequisite: Familiarity with L2CAP channels and RFCOMM is recommended. Check the previous posts in this Bluetooth series if needed.

Key Terms in This Post

HFP Headset Profile Audio Gateway Hands-Free Unit SCO eSCO AT Commands Service Level Connection GOEP OBEX OPP FTP RFCOMM BlueZ ofono Linux Bluetooth

1. Headset Profile (HSP) vs Hands-Free Profile (HFP) — Key Difference
HSP HFP Feature Comparison

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
⚠️ All HFP control commands travel on a separate RFCOMM data channel using AT commands — the same kind your modem used decades ago. The audio itself travels on a completely separate SCO/eSCO link.

2. HFP Roles — Audio Gateway and Hands-Free Unit
Audio Gateway (AG) Hands-Free Unit (HF) Role Assignment

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:

  • Connects to the cellular network
  • Routes audio to/from the HF unit over SCO/eSCO
  • Responds to AT commands sent by HF
  • Notifies HF of call status, signal strength, battery level

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:

  • Provides speaker and microphone to user
  • Sends AT commands to AG to control calls
  • Displays phone status (signal, battery, caller ID)
  • Can initiate audio transfer to/from AG

3. HFP Connection Types & Protocol Stack
Service Level Connection Audio Connection SCO / eSCO RFCOMM

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:

  • AT+CIND? — query call status
  • AT+CLCC — list current calls
  • ATA — answer incoming call
  • +CLIP: “9876543210” — caller ID push
  • +CIEV: signal=4 — signal strength notification

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:

  • SCO: fixed 64 kbps, no retransmission (older)
  • eSCO: variable codec, retransmission, better quality (modern)
  • HFP 1.6+ uses Wide Band Speech (mSBC codec over eSCO)

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)

Service Level Connection (RFCOMM)

Audio Connection (SCO/eSCO)

4. AT Commands — The Language of HFP
3GPP 27.007 AT+CIND AT+CHLD ATA / ATD

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

5. HFP on Linux — BlueZ + oFono
oFono BlueZ HFP plugin D-Bus RFCOMM socket

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
📌 Audio connection: Once the service level connection is set up and a call begins, the AG sets up an SCO/eSCO link automatically. On Linux, the SCO socket is opened separately using BTPROTO_SCO. oFono handles this through the BlueZ audio API in production systems.

6. GOEP — Generic Object Exchange Profile
GOEP OBEX OPP FTP Push / Pull

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
  • Acts as the object repository
  • Objects can be pushed to it (e.g. receive a file)
  • Objects can be pulled from it (e.g. serve a vCard)
  • Exposes its capabilities via SDP
  • Typical: phone acting as file server
  • Initiates the OBEX session
  • Pushes objects to the server (send a file)
  • Pulls objects from the server (get a file)
  • Discovers server via SDP first
  • Typical: laptop sending a file to phone

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

7. OBEX Operations — How Objects Are Transferred
OBEX CONNECT PUT GET DISCONNECT

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

8. GOEP / OBEX on Linux — BlueZ obexd
obexd obexctl obex-client D-Bus OBEX API

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"

9. Quick Reference — HFP & GOEP
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

Leave a Reply

Your email address will not be published. Required fields are marked *