Bluetooth OPP tutorial bluez programming in c

Bluetooth OPP tutorial— BlueZ Programming Guide
Object Push Profile · File Transfer Profile · vCard · vCalendar · OBEX Operations · Full BlueZ C Code on Linux
📤
OPP Push
📁
FTP Browse
📇
vCard / vCal
🐧
BlueZ obexd

What This Post Covers

Hello students welcome to this free embedded systems course by embedded pathahala, this Bluetooth OPP tutorial is part of our free bluetooth development course, This post covers two Bluetooth Classic profiles that are essential for data exchange between devices: OPP (Object Push Profile) and FTP (File Transfer Profile). Both are built on top of GOEP and OBEX, which were covered in the previous post. Here we go one level higher and look at what each profile actually lets you do, what object formats they support, and how to implement them on Linux using BlueZ and obexd.

Every section includes working BlueZ terminal commands and C code examples you can compile and run on Ubuntu right now.

💡 This post is part of the Bluetooth Classic series. The GOEP and OBEX foundation is covered in the previous post. Read that first if you are new to Bluetooth object transfer.

Key Terms in This Post

OPP Object Push Profile FTP File Transfer Profile Push Server Push Client vCard vCalendar vMessage vNote OBEX PUT OBEX GET SETPATH Folder Browse obexd obexctl BlueZ GOEP RFCOMM

1. Where OPP and FTP Fit in the Bluetooth Stack
Profile Hierarchy GOEP Dependency SPP GAP

Both OPP and FTP are application-level profiles. They do not define new transport protocols — they define which existing protocols to use and which specific OBEX operations to allow. Both depend on GOEP, which in turn depends on SPP (Serial Port Profile) and GAP (Generic Access Profile).

Figure 1 — OPP and FTP Profile Dependency Chain

📤 OPP
Object Push Profile
📁 FTP
File Transfer Profile
GOEP — Generic Object Exchange Profile
(provides OBEX session management)
SPP
Serial Port Profile
GAP
Generic Access Profile
OBEX  (Adopted from IrOBEX)
RFCOMM
L2CAP
Lower Layers — LMP · Baseband · Radio
SDP is also used by both profiles to advertise and discover services

OPP vs FTP — What Each Profile Adds

Capability OPP FTP
Push an object/file to device
Pull a specific object from device ✅ (default vCard only) ✅ (any file)
Browse folder structure
Navigate into subfolders
Delete files on remote device
Create folders on remote device
Authentication required Optional Optional
💡 Simple rule: Use OPP when you want to quickly send a contact, calendar event, or small file to another device — no browsing needed. Use FTP when you want full file system access on the remote device.

2. OPP — Object Push Profile in Detail
Push Server Push Client Object Push Business Card Exchange

OPP is the simplest Bluetooth data transfer profile. Its main job is to let one device drop an object (file, contact, calendar entry) into another device’s inbox — just like dropping something in someone’s physical inbox without needing access to their whole filing cabinet. No folder browsing, no authentication negotiation — just push and done.

OPP Roles — Push Server and Push Client

📥 Push Server 📤 Push Client
  • Receives incoming objects pushed to it
  • Stores them in an inbox folder
  • May optionally provide a default vCard (business card) for clients to pull
  • Registers its service via SDP so clients can find it
  • Typical: Mobile phone, tablet
  • Initiates the OBEX session
  • Pushes objects to the server’s inbox
  • Can also pull the server’s default vCard
  • Discovers server via SDP first
  • Typical: Laptop, another phone, PC

OPP Three Core Operations

Figure 2 — OPP Three Operations

1. Object Push

Client sends an object to the server’s inbox using OBEX PUT.

OBEX CONNECT PUT (object data) 200 OK DISCONNECT

Example: Send a contact vCard from laptop to phone

2. Business Card Pull

Client retrieves the server’s default vCard using OBEX GET on the default object.

OBEX CONNECT GET (default vCard) 200 OK + data

Example: Pull the owner’s contact card from a phone

3. Card Exchange

Push + Pull combined in one session. Client pushes its own vCard, then pulls the server’s vCard.

PUT my card GET your card

Example: Two phones swapping contacts when they meet

3. OPP Object Formats — vCard, vCalendar, vMessage, vNote
vCard 2.1 / 3.0 vCalendar vMessage vNote .vcf .vcs

OPP does not define its own data format. Instead it uses four standard text-based formats defined by other standards bodies. All four are human-readable plain text files.

OPP Supported Object Formats

📇 vCard
file extension: .vcf

Used for transferring contact information — name, phone number, email, address, photo. Versions 2.1 and 3.0 are most common in Bluetooth OPP.

BEGIN:VCARD
VERSION:2.1
FN:Ravi Kumar
TEL;CELL:+91-9876543210
EMAIL:ravi@example.com
END:VCARD
📅 vCalendar
file extension: .vcs

Used for transferring calendar events and tasks — appointments, meetings, reminders. Defined by the vCalendar standard (later evolved into iCalendar / .ics).

BEGIN:VCALENDAR
VERSION:1.0
BEGIN:VEVENT
SUMMARY:Team Meeting
DTSTART:20250601T100000
DTEND:20250601T110000
END:VEVENT
END:VCALENDAR
✉️ vMessage
file extension: .vmg

Used for transferring SMS, MMS, or email messages between devices. Defined by the Infrared Mobile Communications (IrMC) specification. Mostly used in feature phones and messaging applications.

BEGIN:VMSG
VERSION:1.1
BEGIN:VBODY
Date:Mon, 01 Jun 2025 10:00:00
From:+919876543210
Hello, this is a test SMS.
END:VBODY
END:VMSG
📝 vNote
file extension: .vnt

Used for transferring plain text notes between devices. Similar in structure to other v-format objects. Used in early smartphone notes applications.

BEGIN:VNOTE
VERSION:1.1
SUMMARY:Shopping List
BODY:Milk, Eggs, Bread, Coffee
END:VNOTE

4. OPP on Linux — BlueZ obexd
obexctl obexd send D-Bus ObjectPush1 Auto-accept

On Linux, OPP is part of the obexd daemon from BlueZ. The client side is exposed through the org.bluez.obex.ObjectPush1 D-Bus interface. The server side is the obexd daemon itself running in auto-accept mode or with an agent.

Push a vCard Using obexctl

# Ensure obexd is running
sudo systemctl start bluetooth
/usr/lib/bluetooth/obexd --auto-accept --root /tmp/opp-inbox &

# Connect and push a vCard to a remote device
obexctl
[obex]# connect 00:1A:7D:DA:71:11     # connects using OPP profile
[00:1A:7D:DA:71:11]# send /home/user/contact.vcf
[00:1A:7D:DA:71:11]# disconnect

# Push a calendar event (.vcs)
obexctl
[obex]# connect 00:1A:7D:DA:71:11
[00:1A:7D:DA:71:11]# send /home/user/meeting.vcs

OPP Push in C — Using D-Bus GIO

/*
 * opp_push.c — Push a vCard to a remote device via OPP using BlueZ D-Bus API
 * Compile: gcc opp_push.c -o opp_push `pkg-config --cflags --libs gio-2.0`
 * Run:     ./opp_push 00:1A:7D:DA:71:11 /home/user/contact.vcf
 *
 * What happens under the hood:
 *   1. obexd opens an RFCOMM connection to the Push Server
 *   2. Sends OBEX CONNECT with OPP target UUID
 *   3. Sends OBEX PUT with file name, file type, file length, and body
 *   4. Receives 200 OK from server
 *   5. Sends OBEX DISCONNECT
 */
#include <gio/gio.h>
#include <stdio.h>
#include <string.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];
    const char *filepath = argv[2];
    GError *error = NULL;

    /* Connect to the session D-Bus (obexd runs on session bus) */
    GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
    if (!conn) {
        fprintf(stderr, "D-Bus session bus error: %s\n", error->message);
        return 1;
    }

    /* Step 1: Open an OBEX session targeting OPP service */
    GVariantBuilder *opts = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
    g_variant_builder_add(opts, "{sv}", "Target",
                          g_variant_new_string("opp"));

    GVariant *session_res = g_dbus_connection_call_sync(
        conn,
        "org.bluez.obex",
        "/org/bluez/obex",
        "org.bluez.obex.Client1",
        "CreateSession",
        g_variant_new("(sa{sv})", bdaddr, opts),
        G_VARIANT_TYPE("(o)"),
        G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error
    );

    if (!session_res) {
        fprintf(stderr, "CreateSession failed: %s\n", error->message);
        return 1;
    }

    const char *session_path;
    g_variant_get(session_res, "(&o)", &session_path);
    printf("OBEX OPP session: %s\n", session_path);

    /* Step 2: Call SendFile on org.bluez.obex.ObjectPush1 interface */
    GVariant *push_res = g_dbus_connection_call_sync(
        conn,
        "org.bluez.obex",
        session_path,
        "org.bluez.obex.ObjectPush1",  /* OPP-specific interface */
        "SendFile",
        g_variant_new("(s)", filepath),
        G_VARIANT_TYPE("(o)"),         /* returns transfer object path */
        G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error
    );

    if (!push_res) {
        fprintf(stderr, "SendFile failed: %s\n", error->message);
    } else {
        const char *transfer_path;
        g_variant_get(push_res, "(&o)", &transfer_path);
        printf("Transfer started: %s\n", transfer_path);
        printf("File '%s' being pushed to %s\n", filepath, bdaddr);
        g_variant_unref(push_res);
    }

    /* Step 3: Remove the session when done */
    g_dbus_connection_call_sync(
        conn, "org.bluez.obex", "/org/bluez/obex",
        "org.bluez.obex.Client1", "RemoveSession",
        g_variant_new("(o)", session_path),
        NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL
    );

    g_variant_unref(session_res);
    g_variant_builder_unref(opts);
    g_object_unref(conn);
    return 0;
}

OPP Server — Receive Incoming Pushes

/*
 * opp_server.c — Accept incoming OPP pushes using raw RFCOMM
 * Compile: gcc opp_server.c -o opp_server -lbluetooth
 * Run:     sudo ./opp_server
 *
 * For production, use obexd --auto-accept which handles OBEX framing.
 * This example shows the raw socket layer underneath.
 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>

#define OPP_CHANNEL  9    /* OPP typically uses channel 9 — check SDP */
#define BUFSIZE      4096

int main() {
    struct sockaddr_rc loc_addr = {0}, rem_addr = {0};
    char buf[BUFSIZE];
    int server_sock, client_sock;
    socklen_t opt = sizeof(rem_addr);

    server_sock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

    loc_addr.rc_family  = AF_BLUETOOTH;
    loc_addr.rc_channel = OPP_CHANNEL;
    bacpy(&loc_addr.rc_bdaddr, BDADDR_ANY);

    bind(server_sock, (struct sockaddr *)&loc_addr, sizeof(loc_addr));
    listen(server_sock, 1);

    printf("OPP server listening on channel %d ...\n", OPP_CHANNEL);

    client_sock = accept(server_sock,
                         (struct sockaddr *)&rem_addr, &opt);

    char addr[18];
    ba2str(&rem_addr.rc_bdaddr, addr);
    printf("Incoming connection from: %s\n", addr);

    /* Read OBEX data — in real use you parse OBEX packets properly */
    int total = 0;
    int bytes;
    FILE *out = fopen("/tmp/received_object.bin", "wb");
    while ((bytes = read(client_sock, buf, BUFSIZE)) > 0) {
        fwrite(buf, 1, bytes, out);
        total += bytes;
        printf("Received %d bytes so far...\r", total);
        fflush(stdout);
    }
    fclose(out);
    printf("\nDone. %d bytes saved to /tmp/received_object.bin\n", total);

    close(client_sock);
    close(server_sock);
    return 0;
}
  1. Find OPP channel: sdptool browse local | grep -A5 "OBEX Object Push"
  2. Compile: gcc opp_server.c -o opp_server -lbluetooth
  3. Run: sudo ./opp_server
  4. Push from phone to your Linux machine — file saves to /tmp/received_object.bin

5. FTP — File Transfer Profile in Detail
FTP Server FTP Client Browse Filesystem SETPATH Delete / Create Folder

FTP goes much further than OPP. Where OPP just drops files into an inbox, FTP gives the client full access to the server’s file system — browse folders, navigate into subfolders, download any file, upload files into any folder, create new folders, and delete files. It uses the same OBEX protocol as OPP but adds the SETPATH operation for folder navigation.

FTP Operations — What OBEX Commands Are Used

FTP Feature OBEX Operation What Happens
Browse folder GET (folder listing) Retrieves an XML folder listing from the server showing file names, sizes, dates
Enter a subfolder SETPATH Changes current working directory on the server
Go up one level SETPATH (backup flag) Moves to parent directory on the server
Download a file GET (filename) Retrieves a specific file from current folder on server
Upload a file PUT (filename + data) Sends a file into the current directory on the server
Delete a file PUT (empty body) Sending PUT with no body and a filename signals deletion
Create a folder SETPATH (create flag) SETPATH with create-folder flag creates a new subdirectory

Figure 3 — FTP Session Flow (Browse + Download)

💻 FTP Client 📁 FTP Server

OBEX CONNECT (FTP Target UUID)

200 OK (Connection ID)

GET (Type: x-obex/folder-listing)

200 OK (XML folder listing)

Client sees: Documents/ Photos/ notes.txt report.pdf …

SETPATH “Documents”

200 OK

GET “report.pdf”

100 CONTINUE … 200 OK + file data

✅ report.pdf downloaded to client

6. FTP on Linux — BlueZ obexd FileTransfer1
FileTransfer1 ListFolder GetFile PutFile ChangeFolder

BlueZ exposes the FTP client through the org.bluez.obex.FileTransfer1 D-Bus interface on the session object. The server side is handled by obexd running as a daemon.

FTP Browse and Download Using obexctl

# Start obexctl
obexctl

# Connect using FTP profile (different from OPP)
[obex]# connect 00:1A:7D:DA:71:11 ftp

# List files in root folder of remote device
[00:1A:7D:DA:71:11]# ls

# Navigate into a subfolder
[00:1A:7D:DA:71:11]# cd Documents

# Download a file from remote device to /tmp/
[00:1A:7D:DA:71:11]# get report.pdf /tmp/report.pdf

# Upload a file to the current folder on remote device
[00:1A:7D:DA:71:11]# put /home/user/photo.jpg photo.jpg

# Create a new folder on remote device
[00:1A:7D:DA:71:11]# mkdir NewFolder

# Delete a file on remote device
[00:1A:7D:DA:71:11]# rm oldfile.txt

# Go back up
[00:1A:7D:DA:71:11]# cd ..

[00:1A:7D:DA:71:11]# disconnect

FTP in C — Browse and Download via D-Bus

/*
 * ftp_browse.c — Connect via FTP and list/download files using BlueZ D-Bus API
 * Compile: gcc ftp_browse.c -o ftp_browse `pkg-config --cflags --libs gio-2.0`
 * Run:     ./ftp_browse 00:1A:7D:DA:71:11
 */
#include <gio/gio.h>
#include <stdio.h>

/* Helper: call a D-Bus method on an OBEX session */
static GVariant *obex_call(GDBusConnection *conn,
                            const char *path,
                            const char *iface,
                            const char *method,
                            GVariant   *params) {
    GError *err = NULL;
    GVariant *result = g_dbus_connection_call_sync(
        conn, "org.bluez.obex", path, iface, method,
        params, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err
    );
    if (!result && err) {
        fprintf(stderr, "%s::%s failed: %s\n", iface, method, err->message);
        g_error_free(err);
    }
    return result;
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <bdaddr>\n", argv[0]);
        return 1;
    }

    GError *error = NULL;
    GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
    if (!conn) { fprintf(stderr, "D-Bus error: %s\n", error->message); return 1; }

    /* Step 1: Create OBEX session with FTP target */
    GVariantBuilder *opts = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
    g_variant_builder_add(opts, "{sv}", "Target",
                          g_variant_new_string("ftp"));  /* FTP, not OPP */

    GVariant *sess = obex_call(conn, "/org/bluez/obex",
                               "org.bluez.obex.Client1", "CreateSession",
                               g_variant_new("(sa{sv})", argv[1], opts));
    if (!sess) return 1;

    const char *session_path;
    g_variant_get(sess, "(&o)", &session_path);
    printf("FTP session: %s\n", session_path);

    /* Step 2: List folder contents (root) */
    GVariant *listing = obex_call(conn, session_path,
                                  "org.bluez.obex.FileTransfer1",
                                  "ListFolder", NULL);
    if (listing) {
        GVariantIter *iter;
        GVariant     *child;
        g_variant_get(listing, "(a{sv})", &iter);   /* array of dicts */
        printf("\nRoot folder contents:\n");
        while ((child = g_variant_iter_next_value(iter))) {
            const char *name = NULL;
            GVariant   *name_v = g_variant_lookup_value(child,
                                   "Name", G_VARIANT_TYPE_STRING);
            if (name_v) {
                name = g_variant_get_string(name_v, NULL);
                printf("  %s\n", name);
                g_variant_unref(name_v);
            }
            g_variant_unref(child);
        }
        g_variant_iter_free(iter);
        g_variant_unref(listing);
    }

    /* Step 3: Navigate into a subfolder */
    obex_call(conn, session_path,
              "org.bluez.obex.FileTransfer1",
              "ChangeFolder",
              g_variant_new("(s)", "Documents"));

    /* Step 4: Download a file */
    GVariant *get = obex_call(conn, session_path,
                              "org.bluez.obex.FileTransfer1",
                              "GetFile",
                              g_variant_new("(ss)",
                                  "/tmp/downloaded.pdf",  /* local path */
                                  "report.pdf"));          /* remote name */
    if (get) {
        printf("Download started. Check /tmp/downloaded.pdf\n");
        g_variant_unref(get);
    }

    /* Step 5: Upload a file to the current remote folder */
    GVariant *put = obex_call(conn, session_path,
                              "org.bluez.obex.FileTransfer1",
                              "PutFile",
                              g_variant_new("(ss)",
                                  "/home/user/photo.jpg",  /* local path */
                                  "photo.jpg"));            /* remote name */
    if (put) {
        printf("Upload started.\n");
        g_variant_unref(put);
    }

    /* Step 6: Clean up */
    obex_call(conn, "/org/bluez/obex",
              "org.bluez.obex.Client1", "RemoveSession",
              g_variant_new("(o)", session_path));

    g_variant_unref(sess);
    g_variant_builder_unref(opts);
    g_object_unref(conn);
    return 0;
}
  1. Install deps: sudo apt install libglib2.0-dev
  2. Compile: gcc ftp_browse.c -o ftp_browse `pkg-config --cflags --libs gio-2.0`
  3. Ensure obexd is running: systemctl --user start obex
  4. Pair your device first: bluetoothctl pair 00:1A:7D:DA:71:11
  5. Run: ./ftp_browse 00:1A:7D:DA:71:11

FTP Server — Start obexd to Accept Incoming Connections

# Start obexd as a file server — exposes /home/user/shared as root
/usr/lib/bluetooth/obexd \
  --auto-accept \
  --root /home/user/shared \
  --no-symlinks &

# Check obexd is running
pgrep -a obexd

# Check which RFCOMM channel FTP is registered on via SDP
sdptool browse local | grep -A 10 "OBEX File Transfer"

# Sample output:
# Service Name: OBEX File Transfer
# Protocol Descriptor List:
#   "L2CAP" (0x0100)
#   "RFCOMM" (0x0003)
#     Channel: 10          <-- clients use this channel

7. Quick Reference — OPP vs FTP vs GOEP
Item Profile Details
Protocol used Both OBEX → RFCOMM → L2CAP → ACL
Object push OPP OBEX PUT with object name + body
vCard format OPP BEGIN:VCARD … END:VCARD — file extension .vcf
vCalendar format OPP BEGIN:VCALENDAR … END:VCALENDAR — file extension .vcs
Folder browse FTP only OBEX GET (x-obex/folder-listing)
Navigate folder FTP only OBEX SETPATH “FolderName”
Linux push tool OPP obexctl → send <file>
Linux FTP tool FTP obexctl → connect <addr> ftp → ls / get / put
D-Bus interface (OPP) OPP org.bluez.obex.ObjectPush1
D-Bus interface (FTP) FTP org.bluez.obex.FileTransfer1

Next Up: A2DP & AVRCP — Bluetooth Audio Streaming

The next post covers A2DP audio streaming and AVRCP remote control — codec negotiation, AVDTP signalling, and BlueZ PipeWire integration. Part of the free Bluetooth course on EmbeddedPathashala.

Leave a Reply

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