Bluetooth SDP Explained: How Devices Discover Each Other’s Services

 

Bluetooth SDP Explained: How Devices Discover Each Other’s Services
Service Discovery Protocol · Service Records · UUID · BlueZ sdptool
4.4
Book Section
SDP
Protocol
128-bit
UUID Size
BlueZ
sdptool

When two Bluetooth devices meet for the first time, how does your phone know the speaker supports A2DP audio, or the keyboard supports HID? The answer is SDP — Service Discovery Protocol. Before any data flows, SDP runs in the background and answers one simple question: “What services do you support?”

This post breaks down how SDP works, what a service record looks like, how UUIDs identify services, and how to inspect SDP records on a real Linux machine using BlueZ’s sdptool.

Keywords covered in this post:

SDP Client SDP Server Service Record Service Attribute Attribute ID Attribute Value Service Class UUID 128-bit UUID 16-bit alias sdptool BlueZ SDP

1. What Is SDP and Why Does It Exist?

When you connect a Bluetooth device, the stack needs to answer two questions in the right order:

  1. What services does the remote device offer? — This is SDP’s job.
  2. How do I use those services? — Done by the specific profile (A2DP, HFP, HID…) after SDP finishes.

SDP only answers question 1. Think of it as the directory board in a building — it tells you which office is on which floor, but doesn’t take you there.

Key point: SDP discovers what services are available. Actual data transfer (audio, serial data, HID reports) happens through profiles like A2DP, RFCOMM, HID — after SDP finishes its job.

2. SDP Client–Server Model

SDP uses a client–server architecture. Every device that wants to offer services runs an SDP server. A device that wants to discover those services acts as an SDP client.

Important rule: Only one SDP server is allowed per Bluetooth device. If a device only consumes services and never provides them, it can skip the SDP server entirely and run as a client-only device.

Figure 1 — SDP Client–Server Architecture

SDP Client
(e.g. smartphone)
ServiceSearchRequest
ServiceAttrRequest
ServiceSearchAttrReq

Request →
← Response
L2CAP
PSM=0x0001

SDP Server
(maintains all service records)

Service Record 1
(e.g. A2DP Audio Sink)
0x0001 · ServiceClassIDList
0x0004 · ProtocolDescList

Service Record 2
(e.g. HFP Hands-Free)
0x0001 · ServiceClassIDList
0x0004 · ProtocolDescList

… more records
(SPP, HID, OBEX…)
One SDP server per Bluetooth device · All records stored in server · Client queries on demand

The SDP server listens on a fixed L2CAP channel (PSM = 0x0001). The three main request types are:

ServiceSearchRequest

Find all service records matching a UUID. Returns a list of ServiceRecord Handles.

ServiceAttributeRequest

Get specific attributes from a record you already have the handle for.

ServiceSearchAttributeRequest

Search by UUID and return attributes in one shot. The most commonly used request.

3. Service Records and Service Attributes

Every service a Bluetooth device offers is described by a Service Record stored inside the SDP server. A record is simply a list of Service Attributes. Each attribute is a key–value pair:

  • Attribute ID — a 16-bit unsigned integer that uniquely names the attribute within a record.
  • Attribute Value — a variable-length field whose meaning depends on the Attribute ID and the service class the record belongs to.

Figure 2 — Inside a Service Record (A2DP Audio Sink Example)

Service Record — A2DP Audio Sink  |  Handle: 0x00010001

Attribute ID (16-bit) Attribute Name Attribute Value (variable length)
0x0000 ServiceRecordHandle 0x00010001  (32-bit handle, unique per server)
0x0001 ServiceClassIDList { UUID 0x110B (AudioSink), UUID 0x1203 (GenericAudio) }
0x0004 ProtocolDescriptorList { L2CAP(PSM=0x0019), AVDTP(ver=0x0103) }
0x0005 BrowseGroupList { UUID 0x1002 (PublicBrowseRoot) }
0x0100 ServiceName “Audio Sink”  (UTF-8 string)
0x0311 SupportedFeatures 0x0001  (Headphone category bitmask)
Standard Attribute IDs 0x0000–0x01FF are defined by the Bluetooth SIG · Profile-specific IDs start from 0x0200
Data Element encoding: Inside an SDP PDU, attribute values are encoded in a compact binary format called Data Element — a type-descriptor byte followed by the payload. Types include: nil, unsigned int, signed int, UUID, text string, boolean, sequence, and URL.

4. Service Class

Every service record belongs to a Service Class. The service class definition specifies:

  • Which attributes are mandatory for records of that type.
  • Which attributes are optional.
  • What data types and allowed values each attribute may hold.

For example, the AudioSink service class (part of A2DP) mandates that a record includes ServiceClassIDList, ProtocolDescriptorList, and BrowseGroupList. Without these, an SDP client cannot figure out how to connect.

Key fact: Each service class is assigned a UUID. This UUID is what the SDP client searches for when it wants to find a specific type of service on a remote device.

5. UUID Deep Dive — 128-bit, 16-bit, 32-bit

A UUID (Universally Unique Identifier) is a 128-bit number guaranteed to be globally unique — without any central authority. Every service class, protocol, and profile has its own UUID. The Bluetooth Assigned Numbers document maintains the table of pre-assigned UUIDs for all standard services.

Figure 3 — UUID Formats in Bluetooth SDP

Full 128-bit UUID — always valid, used for custom/vendor profiles
XXXXXXXX – XXXX – XXXX – XXXX – XXXXXXXXXXXX
32 hex digits · 16 bytes · RFC 4122 format

Bluetooth Base UUID — used as the expansion base for all standard service UUIDs
00000000-0000-1000-8000-00805F9B34FB
Defined by the Bluetooth SIG · Bluetooth Core Spec Vol 3 Part B §2.5.1

▼ 16-bit or 32-bit alias expands into the base UUID by substituting into positions 0–3 ▼

16-bit UUID alias
0x110B → AudioSink
Expands to full 128-bit UUID:
0000110B-0000-1000-8000-00805F9B34FB
Formula: 0000XXXX + base UUID suffix

32-bit UUID alias
0x0000110B → AudioSink
Same expansion, uses 32-bit field:
0000110B-0000-1000-8000-00805F9B34FB
Useful when 16-bit space is exhausted

Commonly Used 16-bit Service Class UUIDs (Bluetooth Assigned Numbers)
0x1101 = SPP 0x110A = A2DP Source 0x110B = A2DP Sink 0x111E = HFP 0x1124 = HID 0x1002 = PublicBrowseRoot 0x1200 = DeviceID

The Bluetooth Base UUID is a fixed value defined in the spec. Below is how it looks in BlueZ source code alongside the standard service class UUID macros:

/* Bluetooth Base UUID — Bluetooth Core Spec Vol 3, Part B, Section 2.5.1 */ /* Any 16-bit alias X expands to: 0000XXXX-0000-1000-8000-00805F9B34FB */ #define BT_BASE_UUID \ “00000000-0000-1000-8000-00805F9B34FB” /* Common 16-bit Service Class UUIDs — from BlueZ lib/uuid.h */ #define AUDIO_SOURCE_SVCLASS_ID 0x110A /* A2DP Source */ #define AUDIO_SINK_SVCLASS_ID 0x110B /* A2DP Sink */ #define HANDSFREE_SVCLASS_ID 0x111E /* HFP Hands-Free */ #define SERIAL_PORT_SVCLASS_ID 0x1101 /* SPP Serial Port */ #define OBEX_PUSH_SVCLASS_ID 0x1105 /* OBEX Object Push */ #define HUMAN_INTERFACE_SVCLASS_ID 0x1124 /* HID */ #define PNP_INFO_SVCLASS_ID 0x1200 /* Device Identification */

6. Inspecting SDP Records with BlueZ sdptool

BlueZ ships with sdptool — a command-line tool to query the SDP server of any Bluetooth device, local or remote. It is the go-to tool when debugging why a Bluetooth profile is not connecting.

6.1 Browse Your Own Device’s SDP Records

# List all service records registered on the local Bluetooth adapter $ sdptool browse local # Typical output (abbreviated): Service Name: Audio Sink Service RecHandle: 0x10001 Service Class ID List: “Audio Sink” (0x110b) Protocol Descriptor List: “L2CAP” (0x0100) PSM: 25 “AVDTP” (0x0019) uint16: 0x0103 Service Name: Headset Audio Gateway Service RecHandle: 0x10002 Service Class ID List: “Headset Audio Gateway” (0x1112) Protocol Descriptor List: “L2CAP” (0x0100) “RFCOMM” (0x0003) Channel: 13

6.2 Query a Remote Device

# Browse all services on a remote Bluetooth device (replace MAC) $ sdptool browse XX:XX:XX:XX:XX:XX # Search for a specific service UUID on a remote device $ sdptool search –bdaddr XX:XX:XX:XX:XX:XX 0x110b # A2DP Sink $ sdptool search –bdaddr XX:XX:XX:XX:XX:XX 0x111e # HFP Hands-Free $ sdptool search –bdaddr XX:XX:XX:XX:XX:XX 0x1101 # SPP Serial Port # Get all records with full raw attribute details $ sdptool records XX:XX:XX:XX:XX:XX

6.3 Manually Register an SDP Record (Testing)

# Register a Serial Port Profile (SPP) service record on RFCOMM channel 22 $ sdptool add –channel=22 SP # Register an OBEX Push service record on channel 9 $ sdptool add –channel=9 OPUSH # Delete a record by its handle $ sdptool del 0x10005 # Verify — check what is now registered locally $ sdptool browse local | grep -A5 “Serial Port”
BlueZ 5.x note: In BlueZ 5 and above, sdptool runs in compatibility mode against the legacy SDP daemon. For production code, SDP records are registered through the bluetoothd D-Bus profile registration API, not via sdptool add directly.

7. Browsing SDP Records from Python

You can query SDP records programmatically using Python’s bluetooth module (PyBluez) or via BlueZ D-Bus. The example below finds the RFCOMM channel for an SPP service — exactly what happens internally when you connect to a serial Bluetooth device.

#!/usr/bin/env python3 “”” Find RFCOMM channel for Serial Port Profile (SPP) via SDP query. Requires: Linux + BlueZ + PyBluez (pip install pybluez) “”” import bluetooth TARGET_ADDR = “XX:XX:XX:XX:XX:XX” # replace with actual MAC def find_spp_channel(addr): # SPP UUID — full 128-bit form required by find_service() SPP_UUID = “00001101-0000-1000-8000-00805f9b34fb” # Internally sends a ServiceSearchAttributeRequest over L2CAP PSM 1 services = bluetooth.find_service(uuid=SPP_UUID, address=addr) if not services: print(“No SPP service found on this device”) return None for svc in services: print(f”Service : {svc[‘name’]}”) print(f”Protocol : {svc[‘protocol’]}”) print(f”Channel : {svc[‘port’]}”) # RFCOMM channel from ProtocolDescList print(f”Handle : {svc[‘handle’]}”) # ServiceRecordHandle print() return services[0][‘port’] if __name__ == “__main__”: ch = find_spp_channel(TARGET_ADDR) if ch: print(f”Connect via RFCOMM channel {ch}”)

7.1 Reading Cached SDP Results via BlueZ D-Bus

#!/usr/bin/env python3 “”” Read discovered service UUIDs from bluetoothd’s cache via D-Bus. bluetoothd caches SDP results automatically after pairing/connecting. “”” import dbus bus = dbus.SystemBus() DEVICE_PATH = “/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX” device_obj = bus.get_object(“org.bluez”, DEVICE_PATH) device_props = dbus.Interface(device_obj, “org.freedesktop.DBus.Properties”) # UUIDs property = list of service class UUIDs found during SDP props = device_props.GetAll(“org.bluez.Device1”) uuids = props.get(“UUIDs”, []) print(“Services discovered via SDP:”) for u in uuids: print(f” {u}”) # Example output: # 00001101-0000-1000-8000-00805f9b34fb → SPP # 0000110b-0000-1000-8000-00805f9b34fb → A2DP Sink # 0000111e-0000-1000-8000-00805f9b34fb → HFP Hands-Free
What happens under the hood: bluetooth.find_service() sends a ServiceSearchAttributeRequest PDU over L2CAP PSM 1 to the remote SDP server. The response is a serialized list of service records in Data Element binary format. PyBluez decodes this and returns a plain Python dict per record.

How It All Comes Together

Figure 4 — Full SDP Flow During Bluetooth Connection Setup

1
Inquiry / Scan
Phone (SDP client) discovers headphone (SDP server) via BR/EDR inquiry or BLE scan. Gets device MAC address.

2
L2CAP Connect → PSM 0x0001
Phone opens an L2CAP channel to the headphone. PSM 0x0001 is the fixed, reserved channel for SDP — no discovery needed for this step.

3
ServiceSearchAttributeRequest
Phone asks: “Give me all records matching UUID 0x110B (AudioSink), and return all their attributes in one shot.”
PDU → L2CAP PSM 1 → SDP server on headphone

4
ServiceSearchAttributeResponse
Headphone SDP server replies with a service record. Phone parses it and learns: L2CAP PSM = 0x0019, AVDTP version 1.3.
Response ← serialized Data Element list of attributes

5
SDP Disconnects · Profile Connects
L2CAP PSM 1 closes. Phone now opens L2CAP PSM 0x0019 and runs AVDTP/A2DP for actual audio streaming. SDP is done — it is never used again during this session.
SDP runs only during connection setup · It closes before any profile data begins to flow

8. Summary

What SDP Does

  • Discovers which services a device offers
  • Returns service records on request
  • Identifies services by UUID
  • Tells the client which PSM or RFCOMM channel to use

What SDP Does NOT Do

  • Does not transfer audio, data, or commands
  • Does not stay connected during data transfer
  • Does not enforce security (that’s SMP + link encryption)
  • Does not apply to BLE (BLE uses GATT service discovery)
Quick Reference — Key SDP Facts
L2CAP PSM = 0x0001 One SDP server per device UUID = 128-bit identifier 16-bit alias + Base UUID Service Record = list of Attributes Attribute ID = 16-bit unsigned int sdptool browse local ServiceSearchAttributeRequest SDP-only for Classic BT BLE uses GATT instead

Continue Learning Bluetooth

Next up: RFCOMM — the serial port emulation layer that sits on top of L2CAP and gives applications a familiar socket-like interface for Classic Bluetooth data transfer.

Browse All Posts Bluetooth Series

Leave a Reply

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