跳到主要内容
about

The Local Control module provides functionality for establishing direct communication with ESP devices on the local network, bypassing the ESP RainMaker Cloud. This enables faster response times and offline control capabilities.

Local Control is implemented across three layers:

  • Native Layer: Handles platform-specific session management and secure communication (ESPProvision SDK for iOS, ESP Provisioning SDK for Android)
  • SDK Layer: Provides local control adapters and React Native bridge integration
  • Application Layer: Manages device connections and data exchange

Local Control requires the device to be on the same local network as the mobile app and supports multiple security levels for secure communication.


Local Control

This guide explains how the Local Control module works, focusing on session establishment, security types, data transmission, and how it integrates with the native layer, SDK, and application layer.

User Flow Overview

The Local Control module has three main flows:

  1. Connection Establishment: App requests connection → Native module establishes session → Security handshake → Session ready
  2. Connection Check: App checks if device is connected → Native module verifies session status → Returns connection state
  3. Data Transmission: App sends command → Native module encrypts data → Sends to device → Receives response → Decrypts and returns

Architecture Overview

Components Structure

Native Layer (iOS):
ios/
└── Local Control/
├── ESPLocalControlModule.swift # React Native bridge for iOS local control
└── ESPLocalControlModule.m # Objective-C bridge exports

Native Layer (Android):
android/app/src/main/java/com/app/local_control/
└── ESPLocalControlModule.kt # React Native bridge for Android local control

SDK Layer:
adaptors/
├── interfaces/
│ └── ESPLocalControlInterface.ts # Native module interface
└── implementations/
└── ESPLocalControlAdapter.ts # Local control adapter for SDK integration

1. Connection Establishment Process

The connection establishment process creates a secure session with an ESP device on the local network. This session enables encrypted communication for controlling the device.

Step-by-Step Connection Flow

iOS Connection Implementation

ESPLocalControlModule.swift handles iOS connection:

@objc(connect:baseUrl:securityType:pop:username:resolve:reject:)
func connect(nodeId: String, baseUrl: String, securityType: NSNumber, pop: String?, username: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
// Determine security type and configure ESPDevice
switch securityType {
case 1:
// Security1: Secure connection with proof of possession
if let pop = pop {
espLocalDevice = ESPDevice(name: nodeId, security: .secure, transport: .softap, proofOfPossession: pop)
} else {
reject("error", "Proof of possession is missing", nil)
return
}
case 2:
// Security2: Secure connection with proof of possession and username
if let pop = pop, let username = username {
espLocalDevice = ESPDevice(name: nodeId, security: .secure, transport: .softap, proofOfPossession: pop, username: username)
} else {
reject("error", "Username or password is missing", nil)
return
}
default:
// Security0: Unsecure connection
espLocalDevice = ESPDevice(name: nodeId, security: .unsecure, transport: .softap)
}

// Configure transport layer
espLocalDevice.espSoftApTransport = ESPSoftAPTransport(baseUrl: baseUrl)

// Initialize session
espLocalDevice.initialiseSession(sessionPath: sessionPath) { status in
switch status {
case .connected:
resolve(["status": "success"])
case .failedToConnect(let eSPSessionError):
reject("error", eSPSessionError.description, nil)
case .disconnected:
reject("error", "Failed to establish session", nil)
}
}
}

Android Connection Implementation

ESPLocalControlModule.kt handles Android connection:

@ReactMethod
fun connect(
nodeId: String,
baseUrl: String,
securityType: Int,
pop: String?,
username: String?,
promise: Promise
) {
this.securityType = securityType
this.baseUrl = baseUrl

// Parse baseUrl to extract IP address and port
val address: String
val port: Int
try {
val url = baseUrl.removePrefix("http://")
val urlParts = url.split(":")
address = urlParts[0]
port = urlParts[1].toInt()
} catch (e: Exception) {
promise.reject("INVALID_BASE_URL", "Failed to parse base URL: $baseUrl. Error: ${e.message}")
return
}

val device = EspLocalDevice(nodeId, address, port)

// Initialize session with security configuration
initSession(device, baseUrl, securityType, pop, username, object : ResponseListener {
override fun onSuccess(returnData: ByteArray?) {
localDeviceMap[nodeId] = device
val result = WritableNativeMap().apply {
putString("status", "success")
}
promise.resolve(result)
}

override fun onFailure(e: Exception) {
promise.reject("SESSION_ESTABLISHMENT_FAILED", "Failed to establish session for nodeId: $nodeId. Error: ${e.message}")
}
})
}

React Native Adapter

ESPLocalControlAdapter.ts provides the React Native interface:

connect: async (
nodeId: string,
baseurl: string,
securityType: number,
pop?: string,
username?: string
): Promise<Record<string, any>> => {
try {
// Default username for Security2 if not provided
let _username;
if (!username) {
_username = securityType === 2 ? "wifiprov" : "";
}

const res = await ESPLocalControlModule.connect(
nodeId,
baseurl,
securityType,
pop,
_username
);
return res;
} catch (error) {
throw error;
}
}

2. Security Types

Local Control supports three security levels for establishing secure sessions with ESP devices:

Security Type 0 (Unsecure)

  • Description: No encryption, plain text communication
  • Use Case: Development, testing, or devices on trusted networks
  • Requirements: None
  • iOS: ESPDevice(name: nodeId, security: .unsecure, transport: .softap)
  • Android: Security0()

Security Type 1 (Secure with POP)

  • Description: Encrypted communication using proof of possession (POP)
  • Use Case: Production devices requiring basic security
  • Requirements: Proof of possession (POP) string
  • iOS: ESPDevice(name: nodeId, security: .secure, transport: .softap, proofOfPossession: pop)
  • Android: Security1(pop)

Security Type 2 (Secure with POP + Username)

  • Description: Encrypted communication using proof of possession and username authentication
  • Use Case: Production devices requiring enhanced security
  • Requirements: Proof of possession (POP) string and username
  • Default Username: "wifiprov" (if not provided)
  • iOS: ESPDevice(name: nodeId, security: .secure, transport: .softap, proofOfPossession: pop, username: username)
  • Android: Security2(username, pop)

Security Type Selection

The security type is typically determined by:

  1. Device configuration during provisioning
  2. Device capabilities
  3. Security requirements
// Example: Connect with Security Type 1
await ESPLocalControlAdapter.connect(
"node123",
"http://192.168.1.100:80",
1, // Security Type 1
"ABCD1234" // POP
);

// Example: Connect with Security Type 2
await ESPLocalControlAdapter.connect(
"node123",
"http://192.168.1.100:80",
2, // Security Type 2
"ABCD1234", // POP
"wifiprov" // Username (optional, defaults to "wifiprov")
);

3. Connection Status Check

The isConnected method checks if a device has an established session.

Connection Check Flow

iOS Implementation

@objc(isConnected:resolve:reject:)
func isConnected(nodeId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
// Check if nodeId matches the device name
if espLocalDevice.name != nodeId {
resolve(false)
return
}
// Check session status
resolve(espLocalDevice.isSessionEstablished())
}

Android Implementation

@ReactMethod
fun isConnected(nodeId: String, promise: Promise) {
// Check if device exists in local device map
val isConnected = localDeviceMap.containsKey(nodeId)
promise.resolve(isConnected)
}

React Native Usage

// Check connection status
const connected = await ESPLocalControlAdapter.isConnected("node123");
if (connected) {
console.log("Device is connected");
} else {
console.log("Device is not connected");
}

4. Data Transmission

Once a session is established, data can be sent to the device using the sendData method. All data must be Base64 encoded.

Data Transmission Flow

iOS Data Transmission

@objc(sendData:path:data:resolve:reject:)
func sendData(nodeId: String, path: String, data: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
// Convert input data to Data object
let data: Data = data.data(using: .utf8)!

// Check if data is Base64 encoded
if let data = Data(base64Encoded: data) {
var invoked = false
espLocalDevice.sendData(path: path, data: data, completionHandler: { data, error in
guard !invoked else { return }

if error != nil {
reject("error", error?.description, nil)
invoked = true
return
}

// Return Base64 encoded response
resolve(data!.base64EncodedString())
invoked = true
})
} else {
reject("error", "Data is not base64 encoded.", nil)
}
}

Android Data Transmission

@ReactMethod
@RequiresApi(Build.VERSION_CODES.O)
fun sendData(nodeId: String, endPoint: String, data: String, promise: Promise) {
val device = localDeviceMap[nodeId]

if (device == null) {
promise.reject("DEVICE_NOT_FOUND", "Device with nodeId $nodeId not found")
return
}

// Normalize endpoint path
val normalizedEndPoint = endPoint.removePrefix("/")
val finalUrl = "$baseUrl/$normalizedEndPoint"

// Decode Base64 data
val decodedData: ByteArray = try {
Base64.getDecoder().decode(data)
} catch (e: IllegalArgumentException) {
promise.reject("INVALID_DATA", "Data is not Base64 encoded or invalid")
return
}

// Check if session is established, initialize if needed
if (session == null || !session!!.isEstablished()) {
val capturedSecurityType = this.securityType
initSession(device, baseUrl, capturedSecurityType, "", "wifiprov", object : ResponseListener {
override fun onSuccess(returnData: ByteArray?) {
sendDataToDevice(finalUrl, decodedData, promise)
}

override fun onFailure(e: Exception) {
promise.reject("SESSION_NOT_INITIALIZED", "Failed to initialize session. Error: ${e.message}")
}
})
} else {
sendDataToDevice(finalUrl, decodedData, promise)
}
}

private fun sendDataToDevice(finalUrl: String, data: ByteArray, promise: Promise) {
session?.sendDataToDevice(finalUrl, data, object : ResponseListener {
@RequiresApi(Build.VERSION_CODES.O)
override fun onSuccess(returnData: ByteArray?) {
val encodedResponse = returnData?.let { Base64.getEncoder().encodeToString(it) } ?: ""
promise.resolve(encodedResponse)
}

override fun onFailure(e: Exception?) {
promise.reject("SEND_DATA_FAILED", e?.message ?: "Failed to send data")
}
})
}

React Native Usage

// Example: Send data to device
const dataToSend = {
// Your command data
command: "turn_on",
value: true
};

// Encode to Base64
const base64Data = Buffer.from(JSON.stringify(dataToSend)).toString('base64');

// Send data
const response = await ESPLocalControlAdapter.sendData(
"node123",
"/api/control",
base64Data
);

// Decode response
const responseData = JSON.parse(Buffer.from(response, 'base64').toString());
console.log("Device response:", responseData);

5. Session Management

Sessions are managed per device and persist until the app is closed or the session is explicitly terminated.

Session States

Android tracks session states explicitly:

enum class SessionState {
NOT_CREATED, // Session not created
CREATING, // Session is being created
CREATED, // Session successfully created
FAILED // Session creation failed
}

iOS uses the ESPProvision SDK's session management:

  • Session is established when initialiseSession completes successfully
  • Session status is checked via isSessionEstablished()

Session Lifecycle

  1. Session Creation: Called when connect() is invoked
  2. Session Establishment: Security handshake completes
  3. Session Active: Data can be sent/received
  4. Session Expiry: Session may expire or be invalidated
  5. Session Re-establishment: Automatically re-established when sending data (Android)

Android Session Re-establishment

Android automatically re-establishes sessions if they're not active when sending data:

if (session == null || !session!!.isEstablished()) {
// Re-initialize session
initSession(device, baseUrl, capturedSecurityType, "", "wifiprov", ...)
}

6. Transport Layer

The transport layer handles HTTP communication with ESP devices on the local network.

Android Transport Implementation

EspLocalTransport class handles HTTP POST requests:

inner class EspLocalTransport(private val baseUrl: String) : Transport {
private val workerThreadPool = Executors.newSingleThreadExecutor()
private val cookieManager = CookieManager()

override fun sendConfigData(path: String, data: ByteArray, listener: ResponseListener) {
workerThreadPool.submit {
try {
val returnData = sendPostRequest(path, data)
if (returnData == null) {
listener.onFailure(RuntimeException("Response not received."))
} else {
listener.onSuccess(returnData)
}
} catch (e: Exception) {
listener.onFailure(e)
}
}
}

private fun sendPostRequest(path: String, data: ByteArray): ByteArray? {
val normalizedPath = if (path.startsWith("http")) URL(path).path else path
val url = URL("$baseUrl/${normalizedPath.removePrefix("/")}")

val urlConnection = url.openConnection() as HttpURLConnection
urlConnection.doOutput = true
urlConnection.requestMethod = "POST"
urlConnection.setRequestProperty("Accept", "text/plain")
urlConnection.setRequestProperty("Content-type", "application/x-www-form-urlencoded")
urlConnection.connectTimeout = 5000
urlConnection.readTimeout = 5000

// Handle cookies for session management
if (cookieManager.cookieStore.cookies.isNotEmpty()) {
urlConnection.setRequestProperty(
"Cookie",
TextUtils.join(";", cookieManager.cookieStore.cookies)
)
}

urlConnection.outputStream.use {
it.write(data)
}

val responseCode = urlConnection.responseCode

// Store cookies from response
val cookiesHeader = urlConnection.headerFields["Set-Cookie"]
cookiesHeader?.forEach { cookie ->
val httpCookie = HttpCookie.parse(cookie)[0]
httpCookie.version = 0
cookieManager.cookieStore.add(null, httpCookie)
}

return if (responseCode == HttpURLConnection.HTTP_OK) {
urlConnection.inputStream.use { it.readBytes() }
} else {
null
}
}
}

iOS Transport Implementation

iOS uses the ESPProvision SDK's ESPSoftAPTransport:

// Configure transport layer
espLocalDevice.espSoftApTransport = ESPSoftAPTransport(baseUrl: baseUrl)

The ESPProvision SDK handles:

  • HTTP POST requests
  • Cookie management
  • Session persistence
  • Error handling

7. Configuration

Local Control is configured in the SDK configuration file:

// rainmaker.config.ts
import ESPLocalControlAdapter from "@/adaptors/implementations/ESPLocalControlAdapter";

export const SDKConfig = {
// ... other config
localControlAdapter: ESPLocalControlAdapter,
};

Native Module Registration

iOS requires Objective-C bridge exports:

// ESPLocalControlModule.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(ESPLocalControlModule, NSObject)

RCT_EXTERN_METHOD(isConnected:(NSString *)nodeId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(connect:(NSString *)nodeId
baseUrl:(NSString *)baseUrl
securityType:(NSNumber *)securityType
pop:(nullable NSString *)pop
username:(nullable NSString *)username
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(sendData:(NSString *)nodeId
path:(NSString *)path
data:(NSString *)data
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

@end

Android automatically registers the module as a ReactPackage:

class ESPLocalControlModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext), ReactPackage {

override fun createNativeModules(
reactContext: ReactApplicationContext
): MutableList<NativeModule> = listOf(this).toMutableList()
}

8. Common Use Cases

Use Case 1: Connect to Device

try {
const result = await ESPLocalControlAdapter.connect(
"node123",
"http://192.168.1.100:80",
1, // Security Type 1
"ABCD1234" // POP
);
console.log("Connected:", result.status);
} catch (error) {
console.error("Connection failed:", error);
}

Use Case 2: Check Connection Status

const isConnected = await ESPLocalControlAdapter.isConnected("node123");
if (isConnected) {
// Device is ready for local control
await sendDeviceCommand();
}

Use Case 3: Send Control Command

// Prepare command
const command = {
device: "light",
action: "set",
params: {
power: true,
brightness: 80
}
};

// Encode to Base64
const base64Command = Buffer.from(JSON.stringify(command)).toString('base64');

// Send command
const response = await ESPLocalControlAdapter.sendData(
"node123",
"/api/control",
base64Command
);

// Decode response
const result = JSON.parse(Buffer.from(response, 'base64').toString());
console.log("Command result:", result);

Use Case 4: Complete Local Control Flow

async function controlDeviceLocally(nodeId: string, baseUrl: string, pop: string) {
try {
// Step 1: Check if already connected
let connected = await ESPLocalControlAdapter.isConnected(nodeId);

if (!connected) {
// Step 2: Connect to device
await ESPLocalControlAdapter.connect(nodeId, baseUrl, 1, pop);
console.log("Device connected");
}

// Step 3: Send control command
const command = { power: true };
const base64Command = Buffer.from(JSON.stringify(command)).toString('base64');
const response = await ESPLocalControlAdapter.sendData(
nodeId,
"/api/control",
base64Command
);

// Step 4: Process response
const result = JSON.parse(Buffer.from(response, 'base64').toString());
return result;
} catch (error) {
console.error("Local control failed:", error);
throw error;
}
}

9. Troubleshooting

Connection Failed

Problem: connect() fails with session establishment error

Solutions:

  1. Verify Device is on Same Network: Ensure the mobile device and ESP device are on the same Wi-Fi network
  2. Check Base URL: Verify the base URL format is correct (http://IP:PORT)
  3. Verify Security Credentials: Ensure POP and username (if required) are correct
  4. Check Device Status: Ensure the ESP device is powered on and accessible
// Debug connection
try {
await ESPLocalControlAdapter.connect(nodeId, baseUrl, securityType, pop, username);
} catch (error) {
console.error("Connection error:", error.message);
// Check error code:
// - "INVALID_BASE_URL": Base URL format is incorrect
// - "SESSION_ESTABLISHMENT_FAILED": Security handshake failed
// - "error": General connection error
}

Device Not Found

Problem: sendData() fails with "Device not found" error

Solutions:

  1. Verify Connection: Ensure connect() was called successfully before sendData()
  2. Check Node ID: Ensure the nodeId matches the one used in connect()
  3. Reconnect: Call connect() again if the session expired
// Ensure device is connected before sending data
const connected = await ESPLocalControlAdapter.isConnected(nodeId);
if (!connected) {
await ESPLocalControlAdapter.connect(nodeId, baseUrl, securityType, pop);
}

Data Encoding Errors

Problem: "Data is not Base64 encoded" error

Solutions:

  1. Encode Data Properly: Ensure all data is Base64 encoded before sending
  2. Check Data Format: Verify the data structure matches device expectations
// Correct Base64 encoding
const data = { command: "turn_on" };
const base64Data = Buffer.from(JSON.stringify(data)).toString('base64');

// Incorrect - will fail
const wrongData = JSON.stringify(data); // Not Base64 encoded

Session Expiry

Problem: Session expires between operations

Solutions:

  1. Android: Sessions are automatically re-established when sending data
  2. iOS: Check session status and reconnect if needed
// iOS: Check and reconnect if needed
const connected = await ESPLocalControlAdapter.isConnected(nodeId);
if (!connected) {
await ESPLocalControlAdapter.connect(nodeId, baseUrl, securityType, pop);
}

Network Timeout

Problem: Requests timeout

Solutions:

  1. Check Network Connectivity: Ensure device is reachable on local network
  2. Verify Device IP: Ping the device IP to confirm it's accessible
  3. Check Firewall: Ensure no firewall is blocking local network communication

Security Type Mismatch

Problem: Connection fails with security errors

Solutions:

  1. Verify Security Type: Ensure the security type matches device configuration
  2. Check POP: Verify proof of possession is correct
  3. Verify Username: For Security Type 2, ensure username is correct (default: "wifiprov")
// Try different security types
try {
await ESPLocalControlAdapter.connect(nodeId, baseUrl, 2, pop, username);
} catch (error) {
// Fallback to Security Type 1
try {
await ESPLocalControlAdapter.connect(nodeId, baseUrl, 1, pop);
} catch (error2) {
// Fallback to Security Type 0 (unsecure)
await ESPLocalControlAdapter.connect(nodeId, baseUrl, 0);
}
}

10. Best Practices

1. Connection Management

  • Check Connection Before Sending: Always verify connection status before sending data
  • Handle Reconnection: Implement reconnection logic for expired sessions
  • Cache Connection Info: Store baseUrl, securityType, and credentials for quick reconnection

2. Error Handling

  • Wrap Operations in Try-Catch: Always handle potential errors
  • Provide User Feedback: Show appropriate error messages to users
  • Log Errors: Log errors for debugging purposes

3. Data Encoding

  • Always Use Base64: Ensure all data is Base64 encoded
  • Validate Data Format: Verify data structure before encoding
  • Handle Encoding Errors: Catch and handle encoding/decoding errors

4. Performance

  • Reuse Sessions: Don't reconnect unnecessarily
  • Batch Operations: Group multiple operations when possible
  • Timeout Handling: Set appropriate timeouts for network operations

5. Security

  • Use Appropriate Security Type: Use Security Type 1 or 2 for production
  • Protect Credentials: Never hardcode POP or usernames
  • Validate Responses: Verify device responses before processing

Summary

The Local Control module provides a complete solution for direct communication with ESP devices on the local network:

  1. Connection Establishment: Native modules establish secure sessions with devices using ESPProvision/ESP Provisioning SDKs
  2. Security Support: Three security levels (0, 1, 2) for different security requirements
  3. Data Transmission: Encrypted data exchange with Base64 encoding
  4. Session Management: Automatic session management with re-establishment capabilities
  5. Cross-Platform: Unified API across iOS and Android with platform-specific implementations

Local Control enables faster response times and offline capabilities by bypassing the ESP RainMaker Cloud, making it ideal for real-time device control and local automation scenarios.