Skip to main content
Provision WiFi credentials to ESP32 devices over Bluetooth Low Energy using the BluFi protocol.

Import

from lager.blufi import BlufiClient

Overview

BluFi is Espressif’s protocol for configuring WiFi on ESP32 devices over BLE. The BlufiClient class provides a Python interface for:
  • Connecting to ESP32 devices via BLE
  • Negotiating secure communication
  • Scanning for available WiFi networks
  • Provisioning WiFi credentials
  • Monitoring connection status
  • Sending custom data

Methods

MethodDescription
connectByName()Connect to device by BLE name
negotiateSecurity()Establish encrypted communication
requestVersion()Get BluFi firmware version
requestDeviceStatus()Get WiFi connection status
requestDeviceScan()Scan for available WiFi networks
postDeviceMode()Set WiFi operation mode
postStaWifiInfo()Send WiFi credentials
postCustomData()Send custom data to device
getVersion()Get cached version string
getWifiState()Get cached WiFi state
getSSIDList()Get cached scan results

Method Reference

BlufiClient()

Create a BluFi client instance.
from lager.blufi import BlufiClient

client = BlufiClient()
Parameters:
ParameterTypeDescription
blufi_service_uuidstrBluFi service UUID (optional)
blufi_write_char_uuidstrWrite characteristic UUID (optional)
blufi_notif_char_uuidstrNotification characteristic UUID (optional)
blufi_notif_desc_uuidstrNotification descriptor UUID (optional)

connectByName(name, timeout=None)

Connect to a BluFi device by its BLE advertised name.
client = BlufiClient()
success = client.connectByName("BLUFI_DEVICE", timeout=10.0)
if success:
    print("Connected!")
else:
    print("Connection failed")
Parameters:
ParameterTypeDescription
namestrBLE device name to connect to
timeoutfloatConnection timeout in seconds (optional)
Returns: bool - True if connected successfully

negotiateSecurity()

Establish secure encrypted communication with the device.
client.negotiateSecurity()
print("Security negotiated - communication is now encrypted")
After calling this method:
  • All subsequent communication will be AES-encrypted
  • Checksums will be enabled for data integrity
  • Must be called before provisioning credentials

requestVersion()

Request the BluFi protocol version from the device.
client.requestVersion()
# Wait for response
client.wait(0.5)
version = client.getVersion()
print(f"BluFi version: {version}")

getVersion()

Get the cached version string from the last requestVersion() call.
version = client.getVersion()
# Returns string like "1.2"
Returns: str or None - Version string

requestDeviceStatus()

Request current WiFi connection status from the device.
client.requestDeviceStatus()
client.wait(0.5)
state = client.getWifiState()
print(f"WiFi state: {state}")

getWifiState()

Get the cached WiFi state from the last requestDeviceStatus() call.
state = client.getWifiState()
# Returns dict: {'opMode': int, 'staConn': int, 'softAPConn': int}
Returns: dict with keys:
  • opMode - Operation mode (see Mode Constants)
  • staConn - Station connection status
  • softAPConn - SoftAP connection count

requestDeviceScan(timeout=10)

Request the device to scan for available WiFi networks.
client.requestDeviceScan(timeout=15)
networks = client.getSSIDList()
for net in networks:
    print(f"  {net['ssid']}: {net['rssi']} dBm")
Parameters:
ParameterTypeDescription
timeoutfloatScan timeout in seconds (default: 10)

getSSIDList()

Get the cached WiFi scan results.
networks = client.getSSIDList()
# Returns list of dicts: [{'ssid': 'NetworkName', 'rssi': -65}, ...]
Returns: list[dict] - List of networks with SSID and RSSI

postDeviceMode(opMode)

Set the WiFi operation mode.
from lager.blufi.constants import OP_MODE_STA

client.postDeviceMode(OP_MODE_STA)  # Station mode
Parameters:
ParameterTypeDescription
opModeintOperation mode constant

postStaWifiInfo(params)

Send WiFi credentials to the device.
client.postStaWifiInfo({
    'ssid': 'MyNetwork',
    'pass': 'MyPassword123'
})
Parameters:
ParameterTypeDescription
paramsdictDictionary with ssid and pass keys

postCustomData(data)

Send custom data to the device (application-specific).
client.postCustomData(b'\x01\x02\x03\x04')
Parameters:
ParameterTypeDescription
databytearrayCustom data bytes

wait(timeout)

Wait for a specified duration (used between operations).
client.wait(1.0)  # Wait 1 second

startNotify() / stopNotify()

Enable or disable BLE notifications.
client.startNotify()   # Enable notifications
client.stopNotify()    # Disable notifications

Constants

Operation Modes

from lager.blufi.constants import (
    OP_MODE_NULL,      # No WiFi
    OP_MODE_STA,       # Station mode
    OP_MODE_SOFTAP,    # Soft AP mode
    OP_MODE_STASOFTAP  # Station + Soft AP
)

SoftAP Security Types

from lager.blufi.constants import (
    SOFTAP_SECURITY_OPEN,
    SOFTAP_SECURITY_WEP,
    SOFTAP_SECURITY_WPA,
    SOFTAP_SECURITY_WPA2,
    SOFTAP_SECURITY_WPA_WPA2
)

Connection Status

from lager.blufi.constants import (
    STA_CONN_SUCCESS,     # Connected successfully
    STA_CONN_FAIL,        # Connection failed
    STA_CONN_CONNECTING,  # Currently connecting
    STA_CONN_NO_IP        # Connected but no IP address
)

WiFi Failure Reasons

from lager.blufi.constants import (
    WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT,  # 4-way handshake timeout (code 15)
    WIFI_REASON_NO_AP_FOUND,              # Access point not found (code 201)
    WIFI_REASON_HANDSHAKE_TIMEOUT,        # Handshake timeout (code 204)
    WIFI_REASON_CONNECTION_FAIL,          # General connection failure (code 205)
    WIFI_SCAN_FAIL                        # WiFi scan failed (code 11)
)
Use these constants to diagnose WiFi connection failures by checking the staConn field from getWifiState():
from lager.blufi.constants import (
    STA_CONN_SUCCESS,
    STA_CONN_FAIL,
    WIFI_REASON_NO_AP_FOUND
)

# After connection attempt, check status
client.requestDeviceStatus()
client.wait(0.5)
state = client.getWifiState()

if state['staConn'] == STA_CONN_SUCCESS:
    print("WiFi connected successfully")
elif state['staConn'] == STA_CONN_FAIL:
    print("WiFi connection failed")

Examples

Basic WiFi Provisioning

from lager.blufi import BlufiClient
from lager.blufi.constants import OP_MODE_STA

def provision_wifi(device_name, ssid, password):
    """Provision WiFi credentials to an ESP32 device."""
    client = BlufiClient()

    # Connect to device
    print(f"Connecting to {device_name}...")
    if not client.connectByName(device_name, timeout=15.0):
        print("Failed to connect")
        return False

    print("Connected!")

    # Negotiate security
    print("Negotiating security...")
    client.negotiateSecurity()
    client.wait(0.5)

    # Get version
    client.requestVersion()
    client.wait(0.5)
    print(f"BluFi version: {client.getVersion()}")

    # Set to station mode
    print("Setting station mode...")
    client.postDeviceMode(OP_MODE_STA)
    client.wait(0.5)

    # Send WiFi credentials
    print(f"Sending credentials for {ssid}...")
    client.postStaWifiInfo({
        'ssid': ssid,
        'pass': password
    })

    # Wait for connection
    print("Waiting for WiFi connection...")
    client.wait(5.0)

    # Check status
    client.requestDeviceStatus()
    client.wait(0.5)
    state = client.getWifiState()

    if state['staConn'] == 0:  # Connected
        print("SUCCESS: WiFi connected!")
        return True
    else:
        print("FAILED: WiFi connection failed")
        return False

# Usage
provision_wifi("BLUFI_ESP32", "MyNetwork", "MyPassword123")

WiFi Network Scanner

from lager.blufi import BlufiClient

def scan_wifi_networks(device_name):
    """Scan for WiFi networks via ESP32."""
    client = BlufiClient()

    print(f"Connecting to {device_name}...")
    if not client.connectByName(device_name, timeout=15.0):
        print("Failed to connect")
        return []

    print("Negotiating security...")
    client.negotiateSecurity()
    client.wait(0.5)

    print("Scanning for WiFi networks...")
    client.requestDeviceScan(timeout=15)

    networks = client.getSSIDList()

    print(f"\nFound {len(networks)} networks:")
    for net in sorted(networks, key=lambda x: x['rssi'], reverse=True):
        signal = "Strong" if net['rssi'] > -50 else "Medium" if net['rssi'] > -70 else "Weak"
        print(f"  {net['ssid']:32s} {net['rssi']:4d} dBm ({signal})")

    return networks

# Usage
networks = scan_wifi_networks("BLUFI_ESP32")

Production WiFi Provisioning Test

from lager.blufi import BlufiClient
from lager.blufi.constants import OP_MODE_STA
import time

def production_wifi_test(device_name, test_ssid, test_password, timeout=30):
    """Production test: provision and verify WiFi connection."""
    client = BlufiClient()

    results = {
        'ble_connect': False,
        'security': False,
        'provision': False,
        'wifi_connect': False
    }

    try:
        # Step 1: BLE Connection
        print("Step 1: BLE Connection...")
        if not client.connectByName(device_name, timeout=15.0):
            print("FAIL: BLE connection failed")
            return results
        results['ble_connect'] = True
        print("  PASS: BLE connected")

        # Step 2: Security negotiation
        print("Step 2: Security negotiation...")
        client.negotiateSecurity()
        client.wait(1.0)
        results['security'] = True
        print("  PASS: Security negotiated")

        # Step 3: Provision credentials
        print("Step 3: Provisioning credentials...")
        client.postDeviceMode(OP_MODE_STA)
        client.wait(0.5)
        client.postStaWifiInfo({
            'ssid': test_ssid,
            'pass': test_password
        })
        results['provision'] = True
        print("  PASS: Credentials sent")

        # Step 4: Wait for WiFi connection
        print("Step 4: Waiting for WiFi connection...")
        start_time = time.time()
        while time.time() - start_time < timeout:
            client.wait(2.0)
            client.requestDeviceStatus()
            client.wait(0.5)
            state = client.getWifiState()

            if state['staConn'] == 0:  # Connected
                results['wifi_connect'] = True
                print("  PASS: WiFi connected")
                break
            else:
                print(f"  Waiting... (staConn={state['staConn']})")

        if not results['wifi_connect']:
            print("  FAIL: WiFi connection timeout")

    except Exception as e:
        print(f"ERROR: {e}")

    # Summary
    print("\n=== RESULTS ===")
    for test, passed in results.items():
        status = "PASS" if passed else "FAIL"
        print(f"  {test}: {status}")

    all_passed = all(results.values())
    print(f"\nOVERALL: {'PASS' if all_passed else 'FAIL'}")

    return results

# Usage
production_wifi_test("BLUFI_ESP32", "TestNetwork", "TestPassword", timeout=30)

Custom Data Exchange

from lager.blufi import BlufiClient

def send_custom_command(device_name, command_data):
    """Send custom data to device after provisioning."""
    client = BlufiClient()

    if not client.connectByName(device_name, timeout=15.0):
        return False

    client.negotiateSecurity()
    client.wait(0.5)

    # Send custom data (application-specific)
    # For example, device configuration commands
    client.postCustomData(command_data)

    return True

# Usage - send configuration command
send_custom_command("BLUFI_ESP32", b'\x01\x00\x10')  # Example command

Error Handling

from lager.blufi import BlufiClient
from lager.blufi.exceptions import BluetoothError

try:
    client = BlufiClient()
    if not client.connectByName("DEVICE", timeout=10.0):
        print("Connection failed - device not found")

    client.negotiateSecurity()

except BluetoothError as e:
    print(f"Bluetooth error: {e}")
except TimeoutError:
    print("Operation timed out")
except Exception as e:
    print(f"Unexpected error: {e}")

Exceptions

from lager.blufi.exceptions import (
    BluetoothError,
    ConnectionError,
    RoleError,
    SecurityError
)
ExceptionDescription
BluetoothErrorBase exception for Bluetooth related errors
ConnectionErrorRaised when a connection is unavailable
RoleErrorRaised when a resource is used with mismatched role
SecurityErrorRaised when a security related error occurs
Note: TimeoutError is Python’s built-in timeout exception, not a BluFi-specific exception.

Hardware Requirements

RequirementDescription
GatewayMust have Bluetooth adapter
ESP32Must be running BluFi-enabled firmware
ProximityBLE range (typically < 10m)

Protocol Details

The BluFi protocol uses:
  • BLE GATT: For data transport
  • DH Key Exchange: For session key establishment
  • AES-128: For data encryption
  • CRC-16: For data integrity
Default UUIDs:
  • Service: 0000ffff-0000-1000-8000-00805f9b34fb
  • Write Characteristic: 0000ff01-0000-1000-8000-00805f9b34fb
  • Notification Characteristic: 0000ff02-0000-1000-8000-00805f9b34fb

Notes

  • Always call negotiateSecurity() before sending credentials
  • WiFi scan results are cached until the next scan
  • The client uses a background thread for BLE operations
  • Connection is automatically cleaned up on client destruction
  • The protocol supports MTU sizes up to 512 bytes
  • After provisioning, the device may restart to apply WiFi settings