The Device Provisioning module provides functionality for adding new IoT devices to the ESP RainMaker ecosystem using multiple connection methods. It supports BLE (Bluetooth Low Energy), QR Code scanning, and SoftAP (Wi-Fi Access Point) provisioning methods.
Device provisioning is implemented using the ESP RainMaker SDK with support for different transport protocols and security levels. The module handles device discovery, connection, authentication, and WiFi configuration.
- To know more about this feature, please click here.
Device Provisioning
This guide explains how the Device Provisioning module works, focusing on the major operations and how ESP RainMaker SDK and CDF APIs are used throughout the system.
User Flow Overview
The Device Provisioning module has three main user flows:
- BLE Provisioning: Bluetooth Low Energy device discovery and connection
- QR Code Provisioning: Scan QR codes for device information
- SoftAP Provisioning: Connect to device's Wi-Fi network for provisioning
Architecture Overview
Components Structure
(device)/
├── AddDeviceSelection.tsx # Provisioning method selection
├── ScanBLE.tsx # BLE device scanning and connection
├── ScanQR.tsx # QR code scanning and device creation
├── ScanSoftAP.tsx # SoftAP device discovery and connection
├── POP.tsx # Proof of Possession code input
├── Provision.tsx # WiFi provisioning and device setup
└── Wifi.tsx # WiFi credentials input (referenced)
1. Device Selection (AddDeviceSelection.tsx)
This is the entry point where users choose their preferred provisioning method.
Key Features
// Device provisioning options
const deviceOptions: DeviceOption[] = [
{
icon: <QrCode size={24} color={tokens.colors.primary} />,
label: t("device.addDeviceSelection.qrOption"),
description: t("device.addDeviceSelection.qrDescription"),
onClick: () => router.push("/ScanQR"),
},
{
icon: <Bluetooth size={24} color={tokens.colors.primary} />,
label: t("device.addDeviceSelection.bluetoothOption"),
description: t("device.addDeviceSelection.bluetoothDescription"),
onClick: () => router.push("/ScanBLE"),
},
{
icon: <HouseWifi size={24} color={tokens.colors.primary} />,
label: t("device.addDeviceSelection.softAPOption"),
description: t("device.addDeviceSelection.softAPDescription"),
onClick: () => router.push("/ScanSoftAP"),
},
];
What this does:
- Presents three provisioning methods to users
- Each option has an icon, label, description, and navigation action
- Routes users to the appropriate scanning screen
2. BLE Device Provisioning (ScanBLE.tsx)
BLE Device Discovery
// Get CDF store for user operations
const { store } = useCDF();
const { userStore } = store;
// Scan for BLE devices
const handleBleDeviceScan = async () => {
setIsScanning(true);
try {
const deviceList = await store.userStore.user?.searchESPBLEDevices(1);
if (deviceList) {
setScannedDevices(deviceList as unknown as ESPDevice[]);
}
} catch (error) {
console.log(error);
} finally {
setIsScanning(false);
}
};
BLE Device Connection
const handleBleDeviceConnect = async (device: ESPDevice) => {
setConnectingDevice((prev) => ({
...prev,
[device.name]: true,
}));
try {
// Store connected device for next screens
store.nodeStore.connectedDevice = device;
// Connect to the device
const response = await device.connect();
// Check for successful connection (0 typically means success)
if (response === 0) {
// Get device capabilities
const capabilities = await device.getDeviceCapabilities();
// Check if device needs POP (Proof of Possession)
if (!capabilities.includes("no_pop") || !capabilities.includes("no_sec")) {
router.push({ pathname: "/(device)/POP" });
return;
}
// Initialize session and proceed to WiFi
await device.initializeSession();
router.push({ pathname: "/(device)/Wifi" });
return;
}
} catch (error) {
toast.showError(t("device.errors.connectionFailed"));
} finally {
setConnectingDevice((prev) => ({
...prev,
[device.name]: false,
}));
}
};
What this does:
- Scans for ESP devices using BLE
- Connects to selected device
- Checks device capabilities for security requirements
- Routes to POP screen if authentication needed, otherwise to WiFi
Customer ID Validation
The ESP RainMaker SDK automatically filters BLE devices by Customer ID during device discovery. This ensures that only devices belonging to the same customer/organization are discoverable and provisionable.
How Customer ID Filtering Works:
// SDK filters devices by customer ID from advertisement data
// The Customer ID is automatically retrieved from the logged-in user object
const deviceList = await store.userStore.user?.searchESPBLEDevices(1);
Customer ID Source:
The Customer ID used for filtering comes from the logged-in user's account/profile. When user.searchESPBLEDevices() is called:
- The SDK internally retrieves the Customer ID from the authenticated user object (
store.userStore.user) - This Customer ID is associated with the user's account in ESP RainMaker Cloud
- The SDK uses this Customer ID to filter discovered BLE devices
Customer ID Flow:
Key Points:
- Customer ID Source: The Customer ID is automatically retrieved from the logged-in user object (
store.userStore.user) - no explicit parameter is passed - Automatic Filtering: The SDK automatically filters devices based on Customer ID embedded in BLE advertisement data (manufacturer data)
- No Manual Validation Required: The app doesn't need to manually validate Customer ID - the SDK handles it transparently
- Advertisement Data: Customer ID is part of the manufacturer-specific data in BLE advertisements
- User Context: The SDK uses the logged-in user's Customer ID (from their account/profile) for comparison
- Security: This prevents users from discovering and provisioning devices that don't belong to their organization
Advertisement Data Structure:
The BLE advertisement contains manufacturer-specific data that includes:
- Customer ID
- Device capabilities
- Security information
- Device type and subtype
Example:
// When scanning for BLE devices
const handleBleDeviceScan = async () => {
setIsScanning(true);
try {
// The Customer ID is automatically retrieved from store.userStore.user
// The SDK internally uses the logged-in user's Customer ID for filtering
// No Customer ID parameter needs to be passed explicitly
// SDK automatically filters by Customer ID
// Only devices matching the user's Customer ID are returned
const deviceList = await store.userStore.user?.searchESPBLEDevices(1);
// All devices in deviceList have matching Customer ID
// The Customer ID comes from:
// - User's account/profile in ESP RainMaker Cloud
// - Automatically retrieved by SDK from user object
// - Compared against Customer ID in device's BLE advertisement data
if (deviceList && deviceList.length > 0) {
setScannedDevices(deviceList as unknown as ESPDevice[]);
}
} catch (error) {
console.log(error);
} finally {
setIsScanning(false);
}
};
Customer ID Details:
- Source: Customer ID is retrieved from the authenticated user's account/profile (
store.userStore.user) - Storage: Customer ID is stored in ESP RainMaker Cloud and associated with the user's account
- Retrieval: SDK automatically retrieves Customer ID from the user object when
searchESPBLEDevices()is called - Comparison: SDK compares the user's Customer ID with Customer ID embedded in each device's BLE advertisement manufacturer data
- No Parameter Required: Customer ID is not passed as a parameter - it's automatically handled by the SDK
Why Customer ID Validation is Important:
- Security: Prevents unauthorized access to devices from other organizations
- Privacy: Ensures users only see devices they're authorized to provision
- Organization Isolation: Maintains separation between different customer deployments
- Compliance: Helps meet enterprise security requirements
Note: Customer ID validation is handled automatically by the ESP RainMaker SDK. No additional configuration or validation code is required in the application layer.
3. QR Code Provisioning (ScanQR.tsx)
QR Code Scanning
// Handle scanned QR code
const handleBarCodeScanned = async (result: any) => {
if (scanned || scannedRef.current) return;
scannedRef.current = true;
setScanned(() => true);
if (result.type === "qr" && result.data) {
const qrData = JSON.parse(result.data);
if (typeof qrData !== "object") {
toast.showError(t("device.scan.qr.invalidQRCode"));
return;
}
setIsProcessing(() => true);
await cameraRef.current?.pausePreview();
// Vibrate to indicate successful scan
if (Platform.OS === "android" || Platform.OS === "ios") {
Vibration.vibrate(200);
}
setTimeout(async () => {
try {
let { security, ver, name, pop, transport } = qrData;
security = security != null ? security : 2;
// Create ESP device with the provided POP code
const deviceInterface = await provisionAdapter.createESPDevice(
name,
transport,
security,
pop
);
if (deviceInterface && deviceInterface.name) {
// Create proper ESPDevice instance from interface
const espDevice = new ESPDevice(deviceInterface);
// Connect and initialize the device
const connectResponse = await espDevice.connect();
if (connectResponse === 0) {
// Store the connected device
store.nodeStore.connectedDevice = espDevice;
// Navigate to WiFi screen
router.push({ pathname: "/(device)/Wifi" });
} else {
toast.showError(t("device.scan.qr.invalidQRCode"));
}
} else {
toast.showError(t("device.scan.qr.invalidQRCode"));
}
} catch (error) {
console.error(error);
toast.showError(t("device.scan.qr.invalidQRCode"));
} finally {
setIsProcessing(false);
setScanned(false);
scannedRef.current = false;
}
}, 10000);
}
};
What this does:
- Scans QR codes using device camera
- Parses QR data for device information
- Creates ESP device instance using provision adapter
- Connects to device and stores in CDF store
- Routes to WiFi configuration screen
4. SoftAP Provisioning (ScanSoftAP.tsx)
Platform-Specific Implementation
// iOS Simple Implementation
const IOSScanSoftAP = () => {
const handleConnect = async () => {
try {
// Check if already connected to SoftAP and get device info
const connectionResult = await ESPSoftAPAdapter.checkSoftAPConnection();
if (connectionResult) {
const { deviceName, capabilities } = connectionResult;
// Check if device needs POP based on capabilities
const needsPop = !capabilities.includes("no_pop") && !capabilities.includes("no_sec");
if (needsPop) {
// Store device info for POP screen
store.nodeStore.softAPDeviceInfo = {
deviceName,
capabilities,
transport: "softap",
};
router.push({ pathname: "/(device)/POP" });
} else {
// No POP required, create ESP device and proceed
const deviceInterface = await provisionAdapter.createESPDevice(
deviceName,
"softap",
2, // security type
"", // no POP needed
"", // softAP password
"wifiprov" // username
);
if (deviceInterface) {
const espDevice = new ESPDevice(deviceInterface);
const response = await espDevice.connect();
if (response === 0) {
store.nodeStore.connectedDevice = espDevice;
router.push({ pathname: "/(device)/Wifi" });
}
}
}
} else {
// Not connected to SoftAP, open WiFi settings
const success = await ESPSoftAPAdapter.openWifiSettings();
}
} catch (error) {
console.error("Error in SoftAP connect:", error);
toast.showError(t("device.errors.connectionFailed"));
}
};
};
// Android Advanced Implementation
const AndroidScanSoftAP = () => {
// Scan for SoftAP devices
const handleSoftAPDeviceScan = async () => {
setIsScanning(true);
setSelectedDevice(null);
try {
const deviceList = await store.userStore.user?.searchESPDevices(
devicePrefix,
ESPTransport.softap
);
if (deviceList) {
setScannedDevices(deviceList);
}
} catch (error) {
console.log("Error scanning SoftAP devices:", error);
} finally {
setIsScanning(false);
}
};
// Connect to selected SoftAP device
const handleSoftAPDeviceConnect = async () => {
if (!selectedDevice) {
toast.showError(t("device.errors.noDeviceSelected"));
return;
}
setConnectingDevice(true);
try {
const response = await selectedDevice.connect();
if (response === 0) {
const deviceCapabilities = await selectedDevice.getDeviceCapabilities();
store.nodeStore.connectedDevice = selectedDevice;
// Check if device requires POP
if (!deviceCapabilities.includes("no_pop") || !deviceCapabilities.includes("no_sec")) {
router.push({ pathname: "/(device)/POP" });
} else {
// Initialize session and navigate to WiFi
const isSessionInitialized = await selectedDevice.initializeSession();
if (isSessionInitialized) {
router.push({ pathname: "/(device)/Wifi" });
}
}
}
} catch (error) {
console.error("Error connecting to SoftAP device:", error);
toast.showError(t("device.errors.connectionFailed"));
} finally {
setConnectingDevice(false);
}
};
};
What this does:
- iOS: Simple flow that checks WiFi connection and guides users
- Android: Full device scanning and selection capabilities
- Both platforms check device capabilities for POP requirements
- Routes to appropriate next screen based on device needs
5. Proof of Possession (POP.tsx)
POP Code Verification
const handleVerify = async () => {
setIsLoading(true);
try {
// Check if this is a SoftAP device
if (softAPDeviceInfo) {
// Create ESP device for SoftAP with the provided POP code
const deviceInterface = await provisionAdapter.createESPDevice(
softAPDeviceInfo.deviceName,
softAPDeviceInfo.transport, // "softap"
2, // security type (SECURITY_2)
popCode
);
if (deviceInterface && deviceInterface.name) {
const espDevice = new ESPDevice(deviceInterface);
const connectResponse = await espDevice.connect();
if (connectResponse === 0) {
store.nodeStore.connectedDevice = espDevice;
store.nodeStore.softAPDeviceInfo = null;
router.push({
pathname: "/(device)/Wifi",
params: { popCode, deviceName: espDevice.name },
});
}
}
} else {
// Regular BLE device flow
await device.setProofOfPossession(popCode);
await device.initializeSession();
router.push({
pathname: "/(device)/Wifi",
params: { popCode, deviceName },
});
}
} catch (e) {
toast.showError(t("device.errors.failedToVerifyCode"));
} finally {
setIsLoading(false);
}
};
What this does:
- Handles POP code verification for both BLE and SoftAP devices
- Creates ESP device instances using provision adapter
- Connects to devices and initializes sessions
- Routes to WiFi configuration screen
6. Device Provisioning (Provision.tsx)
WiFi Provisioning Process
// Provisioning stages configuration
const getProvisionStages = (t: any): ProvisionStage[] => [
{
id: 1,
title: t("device.provision.sendingCredentialsTitle"),
status: "pending",
description: t("device.provision.sendingCredentialsDescription"),
},
{
id: 2,
title: t("device.provision.confirmingConnectionTitle"),
status: "pending",
description: t("device.provision.confirmingConnectionDescription"),
},
{
id: 3,
title: t("device.provision.configuringDeviceAssociationTitle"),
status: "pending",
description: t("device.provision.configuringDeviceAssociationDescription"),
},
{
id: 4,
title: t("device.provision.configuringDeviceAssociationTitle"),
status: "pending",
description: t("device.provision.verifyingDeviceAssociation"),
},
{
id: 5,
title: t("device.provision.settingUpNode"),
status: "pending",
description: t("device.provision.settingUpNodeDescription"),
},
];
// Message to stage mapping
const MESSAGE_STAGE_MAP: Record<string, number> = {
[ESPProvProgressMessages.START_ASSOCIATION]: 1,
[ESPProvProgressMessages.ASSOCIATION_CONFIG_CREATED]: 2,
[ESPProvProgressMessages.SENDING_ASSOCIATION_CONFIG]: 3,
[ESPProvProgressMessages.DEVICE_PROVISIONED]: 4,
[ESPProvProgressMessages.USER_NODE_MAPPING_SUCCEED]: 5,
};
// Start provisioning process
const startProvisioning = async () => {
try {
await device?.provision(
ssid as string,
(password as string) || "",
handleProvisionUpdate
);
} catch (error) {
handleProvisionError(error);
}
};
// Handle provisioning updates
const handleProvisionUpdate = (response: ESPProvResponse) => {
const message = response.description || "";
console.log("Provision update:", message);
switch (response.status) {
case ESPProvResponseStatus.succeed:
updateStageStatus(message);
if (message === ESPProvProgressMessages.USER_NODE_MAPPING_SUCCEED) {
handleProvisionSuccess();
}
break;
case ESPProvResponseStatus.onProgress:
updateStageStatus(message, false);
break;
default:
handleProvisionError(new Error(message));
updateStageStatus(message, true);
break;
}
};
What this does:
- Defines 5 provisioning stages with clear descriptions
- Maps ESP SDK progress messages to UI stages
- Starts WiFi provisioning using device.provision()
- Updates UI stages based on provisioning progress
- Handles success and error scenarios
7. Post-Provisioning Steps
After successful provisioning, the app performs several post-provisioning steps to complete device setup:
Post-Provisioning Flow
const handleProvisionSuccess = async () => {
const nodeId = decodedNodeIdRef.current;
// Step 1: Refresh group to get updated node list
if (currentHomeId) {
await store.userStore.user?.getGroupById({
id: currentHomeId,
withNodeList: true,
});
}
// Step 2: Sync node list if node not found
let provisionedNode = store.nodeStore.nodesByID[nodeId];
if (!provisionedNode) {
await store.nodeStore.syncNodeList();
provisionedNode = store.nodeStore.nodesByID[nodeId];
}
// Step 3: Setup timezone (challenge-response flow)
if (deviceResponse.nodeId) {
await setupNodeTimeZone(nodeId);
}
// Step 4: Configure device services
if (provisionedNode) {
const isAgentDevice = provisionedNode?.nodeConfig?.services?.some(
(s) => s.type === ESPRM_AGENT_AUTH_SERVICE
);
if (isAgentDevice) {
// Set refresh token for AI agent devices
await setRefreshTokenForNode(provisionedNode);
}
// Set user auth for all devices with user-auth service
await setUserAuthForNode(provisionedNode);
// Step 5: Add node to current group
if (currentHomeId && !currentGroup.nodes?.includes(nodeId)) {
await currentGroup.addNodes([nodeId]);
}
}
setIsComplete(true);
};
Key Post-Provisioning Steps
- Refresh Group: Refreshes current group to get updated node list
- Sync Node List: Syncs node list if provisioned node not found
- Setup Timezone: Sets node timezone from user preference (non-blocking)
- Poll Node Details: Polls for node config in challenge-response flow
- Set Refresh Token: Sets refresh token for AI agent devices (agent-auth service)
- Set User Auth: Sets user authentication (refresh token + base URL) for devices with user-auth service
- Add to Group: Adds provisioned node to current home/group
- Guide Redirect: Redirects to guide page if device has readme URL
Note: All post-provisioning steps are non-blocking - errors are logged but don't prevent provisioning completion.
ESP SDK API Usage Summary
1. Device Discovery Operations
// BLE device discovery
const deviceList = await user.searchESPBLEDevices(1);
// SoftAP device discovery
const deviceList = await user.searchESPDevices(prefix, ESPTransport.softap);
// Create ESP device from interface
const deviceInterface = await provisionAdapter.createESPDevice(
name, transport, security, pop, password, username
);
const espDevice = new ESPDevice(deviceInterface);
2. Device Connection Operations
// Connect to device
const response = await device.connect();
// Get device capabilities
const capabilities = await device.getDeviceCapabilities();
// Set proof of possession
await device.setProofOfPossession(popCode);
// Initialize session
await device.initializeSession();
3. Device Provisioning Operations
// Start WiFi provisioning
await device.provision(ssid, password, progressCallback);
// Handle provisioning progress
const handleProvisionUpdate = (response: ESPProvResponse) => {
switch (response.status) {
case ESPProvResponseStatus.succeed:
// Handle success
break;
case ESPProvResponseStatus.onProgress:
// Handle progress
break;
default:
// Handle error
break;
}
};
4. SoftAP Adapter Operations
// Check SoftAP connection
const connectionResult = await ESPSoftAPAdapter.checkSoftAPConnection();
// Open WiFi settings
const success = await ESPSoftAPAdapter.openWifiSettings();
Data Flow Summary
BLE Provisioning Flow:
- AddDeviceSelection.tsx → User selects "Bluetooth"
- ScanBLE.tsx → Scan for BLE devices using
user.searchESPBLEDevices() - Device Selection → User selects device to connect
- Device Connection → Connect using
device.connect() - Capability Check → Check if POP is required
- POP.tsx (if needed) → User enters POP code
- Wifi.tsx → User enters WiFi credentials
- Provision.tsx → Execute provisioning using
device.provision()
QR Code Provisioning Flow:
- AddDeviceSelection.tsx → User selects "QR Code"
- ScanQR.tsx → Scan QR code and parse device data
- Device Creation → Create ESP device using
provisionAdapter.createESPDevice() - Device Connection → Connect using
device.connect() - Wifi.tsx → User enters WiFi credentials
- Provision.tsx → Execute provisioning using
device.provision()
SoftAP Provisioning Flow:
- AddDeviceSelection.tsx → User selects "SoftAP"
- ScanSoftAP.tsx → Platform-specific implementation
- iOS: Check WiFi connection and guide user
- Android: Scan for SoftAP devices
- Device Connection → Connect to selected device
- Capability Check → Check if POP is required
- POP.tsx (if needed) → User enters POP code
- Wifi.tsx → User enters WiFi credentials
- Provision.tsx → Execute provisioning using
device.provision()
Key Takeaways for Developers
- Three Provisioning Methods: BLE, QR Code, and SoftAP
- Platform Differences: iOS and Android have different SoftAP implementations
- Security Requirements: Devices may require POP codes based on capabilities
- Customer ID Validation: SDK automatically filters BLE devices by Customer ID from advertisement data - only devices matching the user's Customer ID are discoverable
- Progressive Stages: Provisioning shows 5 clear progress stages
- Error Handling: Comprehensive error handling for connection and provisioning failures
- Device Storage: Connected devices are stored in
store.nodeStore.connectedDevice - Session Management: Devices require session initialization before provisioning
- Transport Protocols: Support for different transport types (ble, softap, etc.)
Common Patterns
Device Connection Pattern:
try {
const response = await device.connect();
if (response === 0) {
// Connection successful
const capabilities = await device.getDeviceCapabilities();
// Handle capabilities
}
} catch (error) {
// Handle connection error
}
Provisioning Pattern:
await device.provision(ssid, password, (response) => {
switch (response.status) {
case ESPProvResponseStatus.succeed:
// Handle success
break;
case ESPProvResponseStatus.onProgress:
// Update progress
break;
default:
// Handle error
break;
}
});
Error Handling Pattern:
try {
const result = await operation();
if (result === 0) {
// Success
toast.showSuccess("Operation successful");
} else {
// Handle specific error codes
toast.showError("Operation failed");
}
} catch (error) {
console.error("Error:", error);
toast.showError("Operation failed");
}
Device Storage Pattern:
// Store connected device
store.nodeStore.connectedDevice = espDevice;
// Access stored device
const device = store.nodeStore.connectedDevice;
// Clear stored device
store.nodeStore.connectedDevice = null;