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.
Key Terms in This Post
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 |
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 |
|
|
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 |
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 |
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;
}
- Find OPP channel:
sdptool browse local | grep -A5 "OBEX Object Push" - Compile:
gcc opp_server.c -o opp_server -lbluetooth - Run:
sudo ./opp_server - Push from phone to your Linux machine — file saves to
/tmp/received_object.bin
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 | |
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;
}
- Install deps:
sudo apt install libglib2.0-dev - Compile:
gcc ftp_browse.c -o ftp_browse `pkg-config --cflags --libs gio-2.0` - Ensure obexd is running:
systemctl --user start obex - Pair your device first:
bluetoothctl pair 00:1A:7D:DA:71:11 - 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
| 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.
