Chapter 52: Bluetooth SPP Profile Implementation

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the Serial Port Profile (SPP) and its role in Bluetooth Classic communication.
  • Explain how SPP uses RFCOMM and SDP.
  • Differentiate between SPP initiator and acceptor roles.
  • Initialize and configure the SPP module in ESP-IDF.
  • Implement an SPP server on an ESP32 to accept incoming connections.
  • Handle SPP connection events, data reception, and data transmission.
  • Test SPP communication using a standard Bluetooth serial terminal application.
  • Identify ESP32 variants that support SPP.
  • Troubleshoot common issues in SPP implementations.

Introduction

In the previous chapter, we introduced Bluetooth Classic and its fundamental architecture. Now, we delve into one of its most versatile and widely used profiles: the Serial Port Profile (SPP). SPP is designed to emulate a serial cable connection (like an RS-232 link) between two Bluetooth devices, providing a simple and effective way to transmit a stream of data wirelessly.

For embedded systems like the ESP32, SPP is invaluable. It allows the ESP32 to communicate with a vast range of Bluetooth-enabled devices, including smartphones, computers, and other microcontrollers, as if they were connected via a physical serial port. Common applications include:

  • Wireless data logging and sensor reading.
  • Remote control and configuration of devices.
  • Interfacing with legacy systems that expect serial communication.
  • Simple wireless debugging interfaces.

This chapter will guide you through the theory behind SPP and then provide a practical example of implementing an SPP server on the ESP32, allowing it to accept connections and exchange data.

Theory

What is the Serial Port Profile (SPP)?

The Serial Port Profile (SPP) defines the requirements for Bluetooth devices to set up emulated serial cable connections using RFCOMM. Essentially, it provides a wireless replacement for a physical RS-232 serial connection. It offers a simple, byte-stream-oriented interface, making it easy for developers familiar with serial communication to adopt wireless connectivity.

SPP is built upon the Generic Access Profile (GAP) and uses the RFCOMM protocol for data transport. It also relies on the Service Discovery Protocol (SDP) to allow devices to discover available SPP services.

Key Components Used by SPP

  • RFCOMM (Radio Frequency Communication): As discussed in Chapter 51, RFCOMM provides serial port emulation over the L2CAP protocol. It supports multiple simultaneous serial connections (channels) between two devices. Each RFCOMM connection provides a full-duplex communication path. SPP uses a single RFCOMM channel for its data stream.
  • SDP (Service Discovery Protocol): Before an SPP connection can be established, the initiating device needs to discover if the target device offers an SPP service and on which RFCOMM server channel it is listening. This is done using SDP.
    • An SPP server (acceptor) will register an SPP service record with its local SDP server. This record includes a universally unique identifier (UUID) for SPP (0x1101) and the RFCOMM server channel number assigned to the SPP service.
    • An SPP client (initiator) will perform an SDP query on the target device, searching for the SPP service UUID. If found, the SDP response will provide the RFCOMM server channel number needed to establish the connection.

SPP Roles

SPP defines two primary roles, which are analogous to client/server roles:

  1. Device A (Initiator/Client): This device initiates the SPP connection. It performs an SDP query to find the SPP service on Device B and then initiates an RFCOMM connection to the discovered server channel.
  2. Device B (Acceptor/Server): This device accepts incoming SPP connection requests. It registers an SPP service with SDP and listens for incoming RFCOMM connections on a specific server channel.

An ESP32 can be programmed to act as either an SPP initiator or an SPP acceptor. For many embedded applications, the ESP32 acts as an SPP acceptor (server), waiting for a connection from a master device like a smartphone or PC.

SPP Connection Process (Simplified)

sequenceDiagram;
    %% SPP Connection Flow Diagram
    actor Initiator as "SPP Initiator (e.g., Phone/PC)";
    actor Acceptor as "SPP Acceptor (e.g., ESP32)";

    %% Style definitions based on previous color scheme
    %% Primary/Start Nodes: fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
    %% Process Nodes: fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    %% Success Nodes: fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46

    opt Inquiry (if BD_ADDR unknown)
        Initiator->>Initiator: Start Inquiry;
        Initiator-->>Acceptor: Inquiry Packets;
        Acceptor-->>Initiator: FHS Packet (BD_ADDR, CoD);
        Initiator->>Initiator: Collect Responses;
    end

    rect rgb(219, 234, 254)  /* Light Blue for Process Nodes */
        Initiator->>Acceptor: Paging (using BD_ADDR);
        Acceptor->>Initiator: Page Response;
        Note over Initiator,Acceptor: Baseband Connection (ACL Link) Established
    end

    rect rgb(219, 234, 254) /* Light Blue for Process Nodes */
        Initiator->>Acceptor: SDP Query (for SPP Service UUID: 0x1101);
        Acceptor->>Acceptor: SDP Server Lookup;
        Acceptor-->>Initiator: SDP Response (SPP Service Record with RFCOMM Server Channel);
    end

    rect rgb(219, 234, 254) /* Light Blue for Process Nodes */
        Initiator->>Acceptor: RFCOMM Connection Request (to discovered RFCOMM Channel);
        Acceptor->>Acceptor: Process RFCOMM Request;
        Acceptor-->>Initiator: RFCOMM Connection Accepted;
        Note over Initiator,Acceptor: SPP (RFCOMM) Connection Established!
    end

    rect rgb(209, 250, 229) /* Light Green for Success/Data Exchange */
        loop Data Exchange
            Initiator-->>Acceptor: Send Data (byte stream);
            Acceptor-->>Initiator: Send Data (byte stream);
        end
    end

    opt Disconnection
        alt Initiator Initiates
            Initiator->>Acceptor: RFCOMM Disconnection Request;
        else Acceptor Initiates
            Acceptor->>Initiator: RFCOMM Disconnection Request;
        end
        Note over Initiator,Acceptor: RFCOMM Connection Closed
    end

The general flow is:

  1. Discovery (Optional): If the initiator doesn’t know the acceptor’s Bluetooth Device Address (BD_ADDR), it performs an inquiry.
  2. Baseband Connection: The initiator pages the acceptor to establish a baseband link (ACL link).
  3. Service Discovery: The initiator performs an SDP query on the acceptor to find the SPP service and its associated RFCOMM server channel number. The standard UUID for SPP is 0x00001101-0000-1000-8000-00805F9B34FB.
  4. RFCOMM Connection: The initiator requests an RFCOMM connection to the server channel obtained from SDP.
  5. Data Exchange: Once the RFCOMM connection is established, both devices can send and receive data as a byte stream.
  6. Disconnection: Either device can terminate the RFCOMM connection.

Data Flow

SPP provides a full-duplex, reliable data stream. Data sent by one device is received by the other in the order it was sent. RFCOMM handles the underlying segmentation and reassembly of data packets to fit within L2CAP and baseband packet limits. Flow control mechanisms are also part of RFCOMM to prevent buffer overflows.

Security

SPP itself doesn’t define new security mechanisms but relies on the underlying Bluetooth security features provided by GAP (authentication, encryption). When an SPP service is registered or a connection is initiated, security requirements (e.g., authentication required, encryption required) can be specified. Secure Simple Pairing (SSP) is typically used.

Practical Examples

Example 1: ESP32 as an SPP Server (Acceptor)

This example demonstrates how to configure the ESP32 to act as an SPP server. It will:

  1. Initialize the Bluetooth stack (NVS, controller, Bluedroid).
  2. Initialize the SPP module.
  3. Register a callback function to handle SPP events.
  4. Start the SPP server service, making it discoverable and connectable.
  5. When a client connects, it will echo back any data received from the client.

1. Project Setup (VS Code with ESP-IDF Extension):

  1. Create a new ESP-IDF project (e.g., spp_server_demo).
  2. Open sdkconfig.defaults (or create it) and ensure/add these lines:
    CONFIG_BT_ENABLED=y
    CONFIG_BT_BLUEDROID_ENABLED=y
    CONFIG_BT_CLASSIC_ENABLED=y
    CONFIG_BT_SPP_ENABLED=y # Enable SPP Profile
    CONFIG_BT_SSP_ENABLED=y # Enable Secure Simple Pairing
    CONFIG_BT_CTRL_MODE_EFF=2 # ESP_BT_MODE_CLASSIC_BT or 3 for DUAL_MODE
    CONFIG_BT_BLUEDROID_NAME="ESP32_SPP_Server"
    Alternatively, use idf.py menuconfig:
    • Component config -> Bluetooth -> [*] Bluetooth
    • Component config -> Bluetooth -> Bluedroid Options -> [*] Classic Bluetooth
    • Component config -> Bluetooth -> Bluedroid Options -> [*] Serial Port Profile (SPP)
    • Component config -> Bluetooth -> Bluedroid Options -> [*] Secure Simple Pairing
    • Component config -> Bluetooth -> Bluetooth controller -> Bluetooth controller mode -> CLASSIC_BT (or DUAL MODE)
    • Set (ESP32_SPP_Server) Default Bluedroid Host name under Bluedroid Options.

2. C Code Implementation (main/spp_server_main.c):

C
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_bt_api.h"
#include "esp_bt_device.h"
#include "esp_spp_api.h" // ESP32 SPP API

static const char *TAG = "SPP_SERVER_DEMO";

// SPP Server Name (will be advertised over SDP)
#define SPP_SERVER_NAME "ESP32_SPP_SERVICE"
// Default SPP security settings (authentication and encryption)
#define SPP_SEC_MASK (ESP_SPP_SEC_AUTHENTICATE | ESP_SPP_SEC_ENCRYPT)
// SPP Role: Acceptor/Server
#define SPP_ROLE_SERVER ESP_SPP_ROLE_SLAVE // ESP_SPP_ROLE_SLAVE is for acceptor role

// GAP callback function (similar to Chapter 51)
static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param);
// SPP callback function
static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param);

void app_main(void)
{
    esp_err_t ret;
    char bda_str[18]; // Buffer for Bluetooth Device Address string

    // Initialize NVS - required for Bluetooth
    ESP_LOGI(TAG, "Initializing NVS...");
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
    ESP_LOGI(TAG, "NVS initialized.");

    // Release Bluetooth controller memory if it was allocated before
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));

    // Initialize Bluetooth Controller
    ESP_LOGI(TAG, "Initializing Bluetooth controller...");
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Initialize controller failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "Bluetooth controller initialized.");

    // Enable Bluetooth Controller in Classic Bluetooth mode
    ESP_LOGI(TAG, "Enabling Bluetooth controller in Classic BT mode...");
    ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Enable controller failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "Bluetooth controller enabled in Classic BT mode.");

    // Initialize Bluedroid Host Stack
    ESP_LOGI(TAG, "Initializing Bluedroid host stack...");
    ret = esp_bluedroid_init();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Initialize Bluedroid failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "Bluedroid host stack initialized.");

    // Enable Bluedroid Host Stack
    ESP_LOGI(TAG, "Enabling Bluedroid host stack...");
    ret = esp_bluedroid_enable();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "Bluedroid host stack enabled.");

    // Register GAP callback function
    ESP_LOGI(TAG, "Registering GAP callback...");
    ret = esp_bt_gap_register_callback(esp_bt_gap_cb);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "GAP callback register failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "GAP callback registered.");

    // Register SPP callback function
    ESP_LOGI(TAG, "Registering SPP callback...");
    ret = esp_spp_register_callback(esp_spp_cb);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "SPP callback register failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "SPP callback registered.");

    // Initialize SPP module. ESP_SPP_MODE_CB means events are reported via callback.
    ESP_LOGI(TAG, "Initializing SPP module in callback mode...");
    ret = esp_spp_init(ESP_SPP_MODE_CB);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "SPP init failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(TAG, "SPP module initialized.");

    // Set device name (can also be set via menuconfig)
    const char *device_name_str = "ESP32_SPP_Server_Device";
    ESP_LOGI(TAG, "Setting device name to '%s'...", device_name_str);
    ret = esp_bt_dev_set_device_name(device_name_str);
     if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Set device name failed: %s", esp_err_to_name(ret));
    } else {
        ESP_LOGI(TAG, "Device name set successfully.");
    }

    // Set discoverability and connectability mode from GAP
    // ESP_BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE makes the ESP32 visible
    ESP_LOGI(TAG, "Setting device to connectable and discoverable...");
    ret = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Set scan mode failed: %s", esp_err_to_name(ret));
    } else {
        ESP_LOGI(TAG, "Device set to connectable and discoverable.");
    }

    ESP_LOGI(TAG, "SPP Server Example Initialized. Waiting for connections...");

    // Get local Bluetooth device address.
    const uint8_t *bda = esp_bt_dev_get_address();
    if (bda) {
        sprintf(bda_str, "%02X:%02X:%02X:%02X:%02X:%02X", bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);
        ESP_LOGI(TAG, "Local Bluetooth Address: %s", bda_str);
    }
}


// Function to convert BD_ADDR to string for logging
static char *bda_to_str(esp_bd_addr_t bda, char *str, size_t size)
{
    if (bda == NULL || str == NULL || size < 18) {
        return NULL;
    }
    sprintf(str, "%02x:%02x:%02x:%02x:%02x:%02x",
            bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);
    return str;
}

// GAP callback implementation
static void esp_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
    char bda_str[18];
    switch (event) {
        case ESP_BT_GAP_AUTH_CMPL_EVT: { // Authentication complete event
            if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
                ESP_LOGI(TAG, "GAP Authentication success: %s,BDA: %s", param->auth_cmpl.device_name,
                         bda_to_str(param->auth_cmpl.bda, bda_str, sizeof(bda_str)));
            } else {
                ESP_LOGE(TAG, "GAP Authentication failed, status: %d, BDA: %s", param->auth_cmpl.stat,
                         bda_to_str(param->auth_cmpl.bda, bda_str, sizeof(bda_str)));
            }
            break;
        }
        case ESP_BT_GAP_PIN_REQ_EVT: { // Legacy Pairing PIN code request
            ESP_LOGI(TAG, "GAP Legacy PIN request from BDA: %s", bda_to_str(param->pin_req.bda, bda_str, sizeof(bda_str)));
            ESP_LOGI(TAG, "min_16_digit: %d", param->pin_req.min_16_digit);
            if (param->pin_req.min_16_digit) {
                ESP_LOGI(TAG, "Input pin code: 0000000000000000"); // Example for 16-digit PIN
                esp_bt_pin_code_t pin_code = {0}; // Set all to 0
                esp_bt_gap_pin_reply(param->pin_req.bda, true, 16, pin_code);
            } else {
                ESP_LOGI(TAG, "Input pin code: 1234"); // Example for 4-digit PIN
                esp_bt_pin_code_t pin_code;
                pin_code[0] = '1'; pin_code[1] = '2'; pin_code[2] = '3'; pin_code[3] = '4';
                esp_bt_gap_pin_reply(param->pin_req.bda, true, 4, pin_code);
            }
            break;
        }

#if (CONFIG_BT_SSP_ENABLED == true)
        case ESP_BT_GAP_CFM_REQ_EVT: // SSP User Confirmation Request
            ESP_LOGI(TAG, "GAP SSP Confirmation request from BDA: %s", bda_to_str(param->cfm_req.bda, bda_str, sizeof(bda_str)));
            ESP_LOGI(TAG, "Please compare the numeric value: %d", param->cfm_req.num_val);
            // For simplicity in this example, we auto-confirm.
            // In a real application, you would display this to the user for confirmation.
            esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
            break;
        case ESP_BT_GAP_KEY_NOTIF_EVT: // SSP Passkey Notification (display this passkey to the user)
            ESP_LOGI(TAG, "GAP SSP Passkey notification from BDA: %s, passkey:%d",
                     bda_to_str(param->key_notif.bda, bda_str, sizeof(bda_str)),
                     param->key_notif.passkey);
            break;
        case ESP_BT_GAP_KEY_REQ_EVT: // SSP Passkey Request (user enters passkey)
            ESP_LOGI(TAG, "GAP SSP Passkey request from BDA: %s", bda_to_str(param->key_req.bda, bda_str, sizeof(bda_str)));
            // For simplicity, we don't handle passkey input here.
            // In a real application, prompt the user for the passkey.
            // esp_bt_gap_ssp_passkey_reply(param->key_req.bda, true, 123456); // Example passkey
            break;
#endif
        case ESP_BT_GAP_MODE_CHG_EVT:
             ESP_LOGI(TAG, "GAP Mode Changed: mode: %d, BDA: %s", param->mode_chg.mode,
                      bda_to_str(param->mode_chg.bda, bda_str, sizeof(bda_str)));
            break;
        // Handle other GAP events like discovery results if needed
        default: {
            ESP_LOGD(TAG, "Unhandled GAP Event: %d", event);
            break;
        }
    }
}

// SPP callback implementation
static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
{
    char bda_str[18];
    switch (event) {
        case ESP_SPP_INIT_EVT: // SPP Initialized
            if (param->init.status == ESP_SPP_SUCCESS) {
                ESP_LOGI(TAG, "SPP Initialized successfully. Starting server...");
                // Start SPP server. remote_scn = 0 means the stack will assign a free server channel.
                // SPP_SERVER_NAME is the service name that will appear in SDP record.
                esp_spp_start_srvc(SPP_SEC_MASK, SPP_ROLE_SERVER, 0, SPP_SERVER_NAME);
            } else {
                ESP_LOGE(TAG, "SPP Initialization failed, status: %d", param->init.status);
            }
            break;
        case ESP_SPP_UNINIT_EVT: // SPP De-initialized
             ESP_LOGI(TAG, "SPP De-initialized, status: %d", param->uninit.status);
            break;
        case ESP_SPP_DISCOVERY_COMP_EVT: // SDP Discovery Complete (for client mode)
            ESP_LOGI(TAG, "SPP Discovery complete, status: %d, scn_num: %d",
                     param->disc_comp.status, param->disc_comp.scn_num);
            if (param->disc_comp.status == ESP_SPP_SUCCESS) {
                // If we were a client, we could now connect using esp_spp_connect
                // esp_spp_connect(SPP_SEC_MASK, SPP_ROLE_MASTER, param->disc_comp.scn[0], peer_bda_address);
            }
            break;
        case ESP_SPP_OPEN_EVT: // SPP Connection Opened (for client mode)
            ESP_LOGI(TAG, "SPP Connection opened (client), handle: %d, rem_bda: %s",
                     param->open.handle, bda_to_str(param->open.rem_bda, bda_str, sizeof(bda_str)));
            // Connection is open, data can be sent/received
            break;
        case ESP_SPP_CLOSE_EVT: // SPP Connection Closed
            ESP_LOGI(TAG, "SPP Connection closed, status: %d, handle: %d, close_by_remote: %d",
                     param->close.status, param->close.handle, param->close.async);
            // Handle cleanup or prepare for new connections
            break;
        case ESP_SPP_START_EVT: // SPP Server Started
            if (param->start.status == ESP_SPP_SUCCESS) {
                ESP_LOGI(TAG, "SPP Server started successfully, handle: %d, sec_id: %d, scn: %d",
                         param->start.handle, param->start.sec_id, param->start.scn);
                ESP_LOGI(TAG, "Service Name: %s", SPP_SERVER_NAME);
                ESP_LOGI(TAG, "Waiting for client connection...");
            } else {
                ESP_LOGE(TAG, "SPP Server start failed, status: %d", param->start.status);
            }
            break;
        case ESP_SPP_CL_INIT_EVT: // Client Initiated Connection
            ESP_LOGI(TAG, "SPP Client initiated connection, handle: %d, sec_id: %d",
                     param->cl_init.handle, param->cl_init.sec_id);
            // Server can accept or reject. By default, Bluedroid accepts.
            break;
        case ESP_SPP_DATA_IND_EVT: // Data Received
            ESP_LOGI(TAG, "SPP Data received, handle: %d, len: %d",
                     param->data_ind.handle, param->data_ind.len);
            // Log received data as hex and string
            esp_log_buffer_hex(TAG, param->data_ind.data, param->data_ind.len);
            // Ensure null termination for string printing if data is text
            // For this example, we just print. Be careful if data is not null-terminated.
            char temp_buf[param->data_ind.len + 1];
            memcpy(temp_buf, param->data_ind.data, param->data_ind.len);
            temp_buf[param->data_ind.len] = '\0';
            ESP_LOGI(TAG, "Received data string: [%s]", temp_buf);

            // Echo back the received data
            ESP_LOGI(TAG, "Echoing back %d bytes...", param->data_ind.len);
            esp_err_t err = esp_spp_write(param->data_ind.handle, param->data_ind.len, param->data_ind.data);
            if (err != ESP_OK) {
                ESP_LOGE(TAG, "SPP Write failed: %s", esp_err_to_name(err));
            }
            break;
        case ESP_SPP_CONG_EVT: // Congestion Status Changed
            ESP_LOGI(TAG, "SPP Congestion status changed, handle: %d, congested: %s",
                     param->cong.handle, param->cong.cong ? "true" : "false");
            // If congested (true), wait before sending more data.
            // If not congested (false), can resume sending.
            break;
        case ESP_SPP_WRITE_EVT: // Write Operation Complete
            // This event confirms that data passed to esp_spp_write has been sent to the lower layer.
            // It doesn't mean the remote device has received it yet.
            if (param->write.status == ESP_SPP_SUCCESS) {
                 ESP_LOGI(TAG, "SPP Write successful, handle: %d, len: %d, cong: %s",
                         param->write.handle, param->write.len, param->write.cong ? "congested" : "not congested");
                if (param->write.cong) {
                    ESP_LOGW(TAG, "SPP link congested after write. Consider pausing further writes.");
                }
            } else {
                ESP_LOGE(TAG, "SPP Write failed, status: %d, handle: %d", param->write.status, param->write.handle);
            }
            break;
        case ESP_SPP_SRV_OPEN_EVT: // Server Connection Opened (after client connects)
            ESP_LOGI(TAG, "SPP Server connection opened, handle: %d, rem_bda: %s",
                     param->srv_open.handle, bda_to_str(param->srv_open.rem_bda, bda_str, sizeof(bda_str)));
            ESP_LOGI(TAG, "Client Address: %s", bda_to_str(param->srv_open.rem_bda, bda_str, sizeof(bda_str)));
            ESP_LOGI(TAG, "Ready to receive data.");
            // Example: Send a welcome message
            const char *welcome_msg = "Welcome to ESP32 SPP Server!\r\n";
            esp_spp_write(param->srv_open.handle, strlen(welcome_msg), (uint8_t *)welcome_msg);
            break;
        default:
            ESP_LOGD(TAG, "Unhandled SPP Event: %d", event);
            break;
    }
}

Key ESP-IDF SPP Callback Events:

SPP Event (esp_spp_cb_event_t) Description Key Parameters in param union
ESP_SPP_INIT_EVT Triggered when SPP module initialization is complete after esp_spp_init(). param->init.status (Success/failure of initialization)
ESP_SPP_UNINIT_EVT Triggered when SPP module de-initialization is complete after esp_spp_deinit(). param->uninit.status (Success/failure of de-initialization)
ESP_SPP_START_EVT Triggered when an SPP server service has started successfully after esp_spp_start_srvc(). param->start.status (Success/failure)
param->start.handle (Server connection handle)
param->start.scn (Server channel number assigned)
ESP_SPP_SRV_OPEN_EVT Triggered on the SPP server when a client establishes an RFCOMM connection to it. This is a key event for servers. param->srv_open.status (Success/failure)
param->srv_open.handle (Connection handle for this client)
param->srv_open.rem_bda (BD_ADDR of the connected client)
ESP_SPP_DATA_IND_EVT Triggered when data is received over an SPP connection. param->data_ind.handle (Connection handle)
param->data_ind.len (Length of received data)
param->data_ind.data (Pointer to received data buffer)
ESP_SPP_WRITE_EVT Triggered after an esp_spp_write() operation completes, indicating data has been passed to the lower layer. param->write.status (Success/failure)
param->write.handle (Connection handle)
param->write.len (Length of data written)
param->write.cong (Congestion status: true if congested)
ESP_SPP_CLOSE_EVT Triggered when an SPP connection is closed (either locally or by the remote device). param->close.status (Reason for closure)
param->close.handle (Connection handle of the closed connection)
param->close.async (True if closed by remote, false if by local host)
ESP_SPP_CONG_EVT Indicates a change in the congestion status of the SPP link. param->cong.handle (Connection handle)
param->cong.cong (Congestion status: true if congested, false otherwise)
ESP_SPP_DISCOVERY_COMP_EVT (Primarily for SPP Client) Triggered when SDP discovery for SPP services on a remote device is complete after esp_spp_start_discovery(). param->disc_comp.status (Success/failure)
param->disc_comp.scn_num (Number of SPP server channels found)
param->disc_comp.scn[] (Array of found server channel numbers)
ESP_SPP_OPEN_EVT (Primarily for SPP Client) Triggered when an outgoing SPP connection initiated by esp_spp_connect() is established. param->open.status (Success/failure)
param->open.handle (Connection handle)
param->open.rem_bda (BD_ADDR of the connected server)
ESP_SPP_CL_INIT_EVT (Primarily for SPP Server) Triggered when a client initiates a connection to the server, before ESP_SPP_SRV_OPEN_EVT. Allows server to accept/reject. param->cl_init.status (Status of initiation)
param->cl_init.handle (Connection handle)
param->cl_init.sec_id (Security ID used)

3. Build Instructions:

  1. Open a terminal in VS Code.
  2. Ensure your ESP-IDF environment is sourced.
  3. Clean the project: idf.py fullclean
  4. Build the project: idf.py build

4. Run/Flash/Observe:

  1. Connect your ESP32 board.
  2. Flash the firmware: idf.py -p (YOUR_SERIAL_PORT) flash
  3. Open the serial monitor: idf.py -p (YOUR_SERIAL_PORT) monitorYou should see logs indicating Bluetooth and SPP initialization, and finally “SPP Server Example Initialized. Waiting for connections…” and “SPP Server started successfully…”.
  4. Testing with an SPP Client (e.g., Smartphone App):
    • Install a Bluetooth Serial Terminal app on your smartphone (e.g., “Serial Bluetooth Terminal” on Android, or “BlueTerm” or similar on iOS – iOS SPP support can be trickier and often requires MFi program for app store apps, but some generic tools might work).
    • Enable Bluetooth on your phone.
    • Scan for Bluetooth devices. You should see your ESP32 with the name “ESP32_SPP_Server_Device” (or whatever you configured).
    • Pair with the ESP32. You might see SSP confirmation requests on the ESP32’s serial monitor and your phone. Confirm them.
    • Once paired, connect to the “ESP32_SPP_SERVICE” (or the device itself if the app handles SDP lookup implicitly) from the serial terminal app.
    • The ESP32 serial monitor should log an ESP_SPP_SRV_OPEN_EVT event. You should receive the “Welcome to ESP32 SPP Server!” message in your phone app.
    • Type some text in your phone’s serial terminal app and send it.
    • Observe the ESP32’s serial monitor: It will log an ESP_SPP_DATA_IND_EVT with the received data.
    • The ESP32 will then echo this data back. You should see the same text appear in your phone’s serial terminal app.
    • Disconnect from the app. The ESP32 monitor will show an ESP_SPP_CLOSE_EVT.

Tip: When using Android’s “Serial Bluetooth Terminal” app, after pairing, you usually go to “Devices”, select your ESP32, and it should connect. You can then send/receive data. Check the app’s settings for character encoding (usually UTF-8) and line endings if you have issues with text display.

Variant Notes

SPP is a Bluetooth Classic profile. Its availability depends on Bluetooth Classic support in the ESP32 variant:

ESP32 Variant SPP (Serial Port Profile) Support Underlying Bluetooth Requirement Key Notes for SPP
ESP32 (Original) Yes Bluetooth Classic (BR/EDR) Full support. Ideal for SPP applications.
ESP32-S2 No N/A (No Bluetooth) No Bluetooth hardware, thus no SPP.
ESP32-S3 Yes Bluetooth Classic (BR/EDR) Supports SPP. Note BR/EDR packet limitations (BR on 1-slot, EDR on 2/3-DH1) but generally fine for SPP.
ESP32-C2 No BLE Only Does not support Bluetooth Classic, so SPP is not available.
ESP32-C3 No BLE Only Does not support Bluetooth Classic, so SPP is not available.
ESP32-C6 No BLE Only (+802.15.4) Does not support Bluetooth Classic, so SPP is not available.
ESP32-H2 No BLE Only (+802.15.4) Does not support Bluetooth Classic, so SPP is not available.

In summary: For SPP development, use the ESP32 (original) or ESP32-S3.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
SPP Not Enabled in menuconfig Compilation errors: esp_spp_… functions undefined.
Runtime: esp_spp_init() fails or returns an error.
1. Run idf.py menuconfig.
2. Navigate to: Component config -> Bluetooth -> Bluedroid Options.
3. Ensure [*] Serial Port Profile (SPP) is enabled.
4. Save configuration and rebuild the project (idf.py build).
Callback Not Registered or Incorrect SPP events (ESP_SPP_INIT_EVT, ESP_SPP_SRV_OPEN_EVT, etc.) are not triggered or handled.
esp_spp_init() might succeed, but the server doesn’t start or accept connections.
Unexpected behavior or crashes when SPP events occur.
1. Ensure esp_spp_register_callback(your_spp_callback_func) is called after esp_bluedroid_enable() and before esp_spp_init().
2. Verify your callback function signature matches esp_spp_cb_t.
3. Implement logic for relevant cases within the callback’s switch statement.
Client Connection Issues (Pairing/Bonding) Client device (e.g., smartphone app) fails to connect to the ESP32 SPP server.
Connection gets stuck at “connecting…” or repeatedly asks for pairing confirmation.
Authentication failures logged on ESP32.
1. On the client device, “unpair” or “forget” the ESP32 and try pairing again.
2. Ensure SSP is enabled in ESP32’s menuconfig (CONFIG_BT_SSP_ENABLED=y).
3. Check ESP32 serial monitor for GAP events like ESP_BT_GAP_AUTH_CMPL_EVT, ESP_BT_GAP_CFM_REQ_EVT to debug pairing. Confirm SSP requests if prompted.
4. If NVS issues are suspected (old bonding data), try nvs_flash_erase() followed by nvs_flash_init() during testing (will clear all NVS data).
Data Not Sent/Received Correctly Data appears corrupted, incomplete, or is not received on either the ESP32 or client side.
Text messages have incorrect characters or formatting.
1. Verify the len parameter in esp_spp_write() matches the actual data length being sent.
2. Ensure data buffers are adequately sized on both ends.
3. For strings, ensure null termination if expected by the receiver, but pass the correct string length (strlen) to esp_spp_write().
4. Check client terminal app settings for character encoding (UTF-8 is common) and line ending characters (e.g., CR, LF, CR+LF).
5. Use esp_log_buffer_hex(TAG, data, len) on ESP32 to inspect raw byte values.
SPP Server Not Discoverable or Not Starting ESP32 device does not appear in Bluetooth scans on the client.
Client cannot find the specific SPP service advertised by the ESP32.
ESP32 logs show errors during esp_spp_init() or esp_spp_start_srvc().
1. Ensure esp_spp_start_srvc() is called within the ESP_SPP_INIT_EVT case in your SPP callback, only if param->init.status == ESP_SPP_SUCCESS.
2. Verify esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE) is called to make the ESP32 discoverable via GAP.
3. Check the service name (SPP_SERVER_NAME) used in esp_spp_start_srvc(). This is what clients will look for via SDP.
4. Review ESP32 serial logs for any specific error messages during SPP initialization or server start.
Stack Overflows or Crashes during SPP Operations Guru Meditation Errors or other crashes, especially when handling data or many SPP events. 1. Similar to general Bluetooth issues: Keep SPP callback (esp_spp_cb) functions lean. Offload lengthy processing to separate tasks using FreeRTOS queues/events.
2. Be cautious with buffer sizes for received/transmitted data. Avoid large stack allocations in callbacks.
3. If using printf or extensive logging in callbacks, ensure it’s not causing delays or stack issues. Consider using ESP_LOG levels appropriately.

Exercises

  1. Custom Welcome Message and Command Parser:
    • Modify the SPP server example to send a unique welcome message to the client upon connection, including the ESP32’s device name.
    • Enhance the ESP_SPP_DATA_IND_EVT handler to parse simple text commands received from the client. For example:
      • If the client sends “LED_ON”, turn on the ESP32’s built-in LED (if available, typically GPIO2 on many dev boards).
      • If the client sends “LED_OFF”, turn off the LED.
      • If the client sends “STATUS”, the ESP32 responds with “STATUS:OK”.
      • Send an acknowledgment back to the client (e.g., “LED is ON”, “Unknown command”).
  2. Periodic Sensor Data Transmission:
    • Assume you have a sensor connected to the ESP32 (or use a simulated sensor value, like a counter or esp_random()).
    • Modify the SPP server so that once a client is connected, the ESP32 periodically (e.g., every 5 seconds) reads the sensor value and sends it to the connected SPP client as a formatted string (e.g., “Temperature: 25.5 C”).
    • Use a FreeRTOS timer or a dedicated task for periodic sending. Ensure data is only sent if an SPP connection is active (check the connection handle from ESP_SPP_SRV_OPEN_EVT).
  3. SPP Client Implementation (Advanced):
    • Write a new ESP32 application that acts as an SPP client.
    • This client should:
      1. Perform a device discovery (esp_bt_gap_start_discovery()) to find nearby Bluetooth devices.
      2. Allow the user to select a target device (e.g., by BD_ADDR logged to the console, or connect to a hardcoded BD_ADDR of your SPP server from Example 1).
      3. Perform an SDP query (esp_spp_start_discovery()) on the target device to find its SPP service and RFCOMM channel.
      4. Connect to the discovered SPP service using esp_spp_connect().
      5. Once connected, send a predefined message (e.g., “Hello from ESP32 Client!”) and print any data received from the server.
    • This is more complex due to managing discovery states and SDP results.
  4. Connection Handle Management for Multiple Clients (Conceptual):
    • The current SPP server example implicitly handles one client at a time because esp_spp_start_srvc typically sets up one server channel. While RFCOMM supports multiple channels, the basic esp_spp_start_srvc usually creates one instance.
    • Research how you might manage multiple simultaneous SPP client connections if the ESP-IDF SPP API supports it (e.g., if multiple esp_spp_start_srvc calls with different service names or if a single server handle can manage multiplexed client sessions on different RFCOMM DLCI).
    • Outline the changes needed in esp_spp_cb to differentiate between data and events from different connected clients using their unique handle parameter from ESP_SPP_SRV_OPEN_EVT or ESP_SPP_OPEN_EVT.
    • Note: Full implementation might be complex; focus on the conceptual design of tracking and interacting with multiple handles.

Summary

  • SPP emulates a serial port connection over Bluetooth Classic, using RFCOMM for data transport and SDP for service discovery.
  • Devices in an SPP connection act as an Initiator (client) or Acceptor (server).
  • ESP-IDF provides esp_spp_api.h for SPP implementation, requiring initialization and callback registration.
  • Key SPP events include initialization (ESP_SPP_INIT_EVT), server start (ESP_SPP_START_EVT), client connection (ESP_SPP_SRV_OPEN_EVT), data indication (ESP_SPP_DATA_IND_EVT), and connection close (ESP_SPP_CLOSE_EVT).
  • The esp_spp_write() function is used to send data over an established SPP connection.
  • SPP is supported on ESP32 (original) and ESP32-S3, but not on BLE-only or WiFi-only variants like ESP32-S2, C3, C6, H2.
  • Proper configuration in menuconfig and careful event handling in callbacks are crucial for successful SPP implementation.
  • Testing with a standard Bluetooth serial terminal app is an effective way to verify SPP functionality.

Further Reading

Leave a Comment

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

Scroll to Top