If you want to control hardware directly from an Android phone without relying on a local WiFi router or cloud broker, Bluetooth Low Energy (BLE) is the definitive choice. When pairing Android and Raspberry Pi 3 hardware, the Pi 3 Model B+ serves as an excellent BLE peripheral (server) thanks to its onboard Cypress CYW43455 wireless chip. This guide walks through building a BLE-controlled 2-channel relay system, complete with pin mappings, production-ready Python code, and solutions to the notorious BlueZ permission errors that stall most builders.

The Decision Tree: Connecting Android to Raspberry Pi 3

Before wiring anything, you must choose your communication protocol. Builders often default to MQTT over WiFi, but that introduces network dependency and latency. Use this decision matrix to select the right interface for your embedded project.

Protocol Network Dependency Latency Setup Complexity Effective Range
MQTT over WiFi High (Requires Router/Broker) Medium (10-50ms) High (Network config + Broker) ~30m (Indoor)
USB OTG Serial None Lowest (<5ms) Low (Direct cable) 1m (Cable length)
BLE GATT Server None (Direct P2P) Low (15-30ms) Medium (Code + DBus) ~10m (No walls)
The Verdict: Choose BLE GATT. It terminates the decision path here because it provides direct, zero-infrastructure control from any Android device running a generic BLE scanner app (like nRF Connect), completely bypassing WiFi dropouts and router reboots.

Hardware Spec Sheet & Parts List

This build specifically targets the Raspberry Pi 3 Model B+ (Broadcom BCM2837B0, 1GB RAM). Do not use the Pi 3 Model A+ or older Pi 2 boards; the B+ features improved thermal management and the dual-band Wi-Fi/BT 4.2 chip required for stable BLE peripheral advertising.

Component Exact Variant / Model Estimated Cost (2026) Notes
Microcontroller Raspberry Pi 3 Model B+ (1GB) $35 - $45 (Used/Refurb) Must have onboard CYW43455 BT chip.
Power Supply Official 5.1V 2.5A Micro-USB PSU $12 Prevents brownout warnings under relay load.
Relay Module Songle SRD-05VDC-SL-C (2-Channel) $8 Optocoupler isolated, active LOW trigger.
Android Client Any Android 10+ device N/A Use the free nRF Connect app from Play Store.

Pin Mapping & Wiring the Relay Module

The Pi 3's 3.3V GPIO logic is not sufficient to drive 5V relay coils directly. We use the relay module's onboard optocouplers and power the coil side from the Pi's 5V rail. Ensure your relay module is set to Active LOW (the default for most Songle modules with jumper caps installed).

Pi 3 Physical Pin BCM GPIO Number Relay Module Pin Wire Color (Standard)
Pin 2 5V Power VCC Red
Pin 6 Ground GND Black
Pin 11 GPIO 17 IN1 (Relay 1) Yellow
Pin 13 GPIO 27 IN2 (Relay 2) Orange
Safety Warning: If you are switching mains voltage (120V/240V AC) on the relay's Common/NO/NC terminals, de-energize the circuit, verify dead with a CAT III multimeter, and ensure the relay is housed in an insulated, non-conductive enclosure. Never route low-voltage DC logic wires in the same conduit as mains AC.

The Python BLE Server Code (Targeting Pi 3 Model B+)

This script uses the bluezero library, which wraps the complex Linux BlueZ D-Bus API into manageable Python objects. It advertises a custom BLE service with two characteristics (one for each relay). The Android app writes a 1 or 0 to toggle the GPIO pins.

Prerequisites: Run sudo pip3 install bluezero RPi.GPIO on your Pi.

#!/usr/bin/env python3
import sys
import time
import RPi.GPIO as GPIO
from bluezero import peripheral

# --- PIN DEFINITIONS ---
RELAY_1_PIN = 17  # BCM 17 / Physical Pin 11
RELAY_2_PIN = 27  # BCM 27 / Physical Pin 13

# --- BLE UUIDs ---
# Generate your own using `uuidgen` in terminal for production
SERVICE_UUID = '0000fff0-0000-1000-8000-00805f9b34fb'
RELAY1_CHAR_UUID = '0000fff1-0000-1000-8000-00805f9b34fb'
RELAY2_CHAR_UUID = '0000fff2-0000-1000-8000-00805f9b34fb'

def setup_hardware():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(RELAY_1_PIN, GPIO.OUT, initial=GPIO.HIGH) # HIGH = OFF for Active LOW
    GPIO.setup(RELAY_2_PIN, GPIO.OUT, initial=GPIO.HIGH)

def update_relay(characteristic):
    """Callback triggered when Android writes to the BLE characteristic."""
    value = int(characteristic.value[0])
    pin = RELAY_1_PIN if characteristic.uuid == RELAY1_CHAR_UUID else RELAY_2_PIN
    
    # Active LOW logic: 0 turns relay ON, 1 turns relay OFF
    if value == 1:
        GPIO.output(pin, GPIO.LOW)
        print(f'Relay on Pin {pin} ENGAGED')
    else:
        GPIO.output(pin, GPIO.HIGH)
        print(f'Relay on Pin {pin} DISENGAGED')

def main():
    setup_hardware()
    
    # Define the BLE Peripheral
    pi_ble = peripheral.Peripheral(
        adapter_name='hci0',
        local_name='Pi3-Relay-Server',
        appearance=0x0000
    )
    
    # Add Service
    pi_ble.add_service(srv_id=1, uuid=SERVICE_UUID, primary=True)
    
    # Add Characteristics with write callbacks
    pi_ble.add_characteristic(
        srv_id=1, chr_id=1, uuid=RELAY1_CHAR_UUID,
        value=[], notifying=False,
        flags=['read', 'write'],
        write_callback=update_relay,
        read_callback=None
    )
    pi_ble.add_characteristic(
        srv_id=1, chr_id=2, uuid=RELAY2_CHAR_UUID,
        value=[], notifying=False,
        flags=['read', 'write'],
        write_callback=update_relay,
        read_callback=None
    )

    print('Starting BLE Peripheral. Scan with nRF Connect on Android...')
    try:
        pi_ble.publish()
    except KeyboardInterrupt:
        print('\nShutting down...')
    except Exception as e:
        print(f'Fatal BLE Error: {e}')
    finally:
        GPIO.cleanup()
        print('GPIO cleaned up. Safe to power off.')

if __name__ == '__main__':
    main()

Debugging the "AccessDenied" BlueZ Error

When running BLE GATT servers on Linux, the D-Bus message broker strictly enforces permissions. The most common failure mode when executing the script above as a standard user (e.g., pi) is an immediate crash with the following exact error string:

dbus.exceptions.DBusException: org.freedesktop.DBus.Error.AccessDenied: Rejected send message, 1 matched rules; type="method_call", sender=":1.14" (uid=1000 pid=1234 comm="python3") destination="org.bluez" (uid=0 pid=456 comm="/usr/lib/bluetooth/bluetoothd ")

Ranked Causes & Fixes

  1. Cause 1: Missing D-Bus Policy Permissions (Most Likely). The standard user lacks rights to register a GATT application with the root-owned bluetoothd daemon.
    Fix: Run the script with sudo python3 ble_relay.py. For a permanent fix without sudo, create a custom D-Bus policy file in /etc/dbus-1/system.d/ granting the bluetooth group access to the org.bluez interface.
  2. Cause 2: Bluetooth Service Not Active. The bluetoothd daemon crashed or is masked.
    Fix: Run sudo systemctl restart bluetooth and verify with systemctl status bluetooth.
  3. Cause 3: HCI0 Interface Soft-Blocked. The RF kill switch has disabled the Bluetooth radio at the kernel level.
    Fix: Run rfkill list. If Bluetooth shows "Soft blocked: yes", run sudo rfkill unblock bluetooth.

The First Three Things to Check When It Fails

If your Android phone sees the Pi advertising but drops the connection immediately upon writing a value, run through this checklist:

  1. Verify MTU Negotiation: Android devices often request a larger Maximum Transmission Unit (MTU). Ensure your bluezero characteristic isn't hardcoded to a 20-byte payload limit if your app is sending larger arrays. (Our code uses single-byte integer writes, bypassing this issue).
  2. Check Power Supply Brownouts: When the relay coil energizes, it draws ~70mA. If your Pi 3 power supply is marginal, the 5V rail dips, resetting the CYW43455 Bluetooth chip. Check dmesg | grep -i undervoltage. If you see warnings, upgrade to a 3A power supply.
  3. Inspect GPIO State: Use raspi-gpio get in a separate terminal while triggering the Android app to confirm the software is actually toggling the BCM pins, isolating the issue to hardware wiring vs. software logic.

Extending or Simplifying the Build

Depending on your final application, you may need to scale this architecture up or strip it down.

How to Simplify (The "I Just Want a Button" Route)

If setting up custom GATT UUIDs and D-Bus permissions is overkill, scrap bluezero and install the BlueDot library. BlueDot creates a virtual joystick over standard Bluetooth Serial (RFCOMM) rather than BLE. It requires pairing via the Raspberry Pi desktop GUI, but the Python code drops to five lines, and the companion Android app provides a ready-made UI without needing nRF Connect.

How to Extend (Whole-Home Mesh)

The Pi 3's BLE range is limited to about 10 meters through walls. To extend this to a whole-home automation system without running Cat6 cable:

  • Keep the Raspberry Pi 3 as the central logic hub and MQTT broker.
  • Deploy ESP32-C3 modules ($4 each) at the actual relay locations. The ESP32 natively supports BLE and WiFi.
  • Configure the ESP32s as BLE-to-WiFi bridges. The Pi 3 pushes MQTT messages over your home WiFi to the ESP32, which then executes local GPIO toggles. This preserves the Pi's processing power for dashboards and logging while offloading the RF heavy lifting to dedicated microcontrollers.

For deeper reading on the Linux Bluetooth stack architecture, consult the official BlueZ project documentation and the Raspberry Pi hardware compute modules guide for specific thermal and power delivery constraints of the Pi 3 generation.