Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f69135c
WIP for ESPProvisionProvider: device and wifi discovery happy flow wo…
ebariaux Apr 7, 2025
210614a
Implement device disconnect and better handling of connection statuses
ebariaux Apr 7, 2025
80c2940
Whole ESPProvision flow working but code review/cleaning required
ebariaux Apr 8, 2025
74a76d1
Error handling in case of missing keys from webapp messages
ebariaux Apr 11, 2025
781ae02
Handle error during Wifi scan
ebariaux Apr 24, 2025
44cc421
Some better error handling
ebariaux Apr 24, 2025
f39ef13
M1V1-55: Properly handle list of discovered network, avoiding duplica…
ebariaux Apr 24, 2025
9daf3b7
M1V1-61: on permissions granted, only start a scan if prefix is set (…
ebariaux May 13, 2025
bc5c0ef
M1V1-64: Add indication whether exit provisioning was successful or not
ebariaux May 13, 2025
dbcec14
Merge branch 'main' into feature/ESPProvisionProvider
ebariaux May 13, 2025
98ac6d8
Merge branch 'main' into feature/ESPProvisionProvider
ebariaux May 28, 2025
010cbd2
Improve check for BLE permissions and only report back to webapp when…
ebariaux May 29, 2025
5d75d11
Make function suspend to not block the main thread
ebariaux Jun 2, 2025
174f95f
Comment on usage of Main context
ebariaux Jun 2, 2025
4e25452
Clean-up comments
ebariaux Jun 2, 2025
fc01360
Don't need full package prefix
ebariaux Jun 2, 2025
258da48
Some improvements on coroutine contexts usage
ebariaux Jun 2, 2025
20c739a
Properly pass pop received from web app to provider
ebariaux Aug 14, 2025
673082f
Merge branch 'main' into feature/ESPProvisionProvider
ebariaux Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ORLib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ dependencies {
implementation platform('com.google.firebase:firebase-bom:33.12.0')
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'com.github.espressif:esp-idf-provisioning-android:lib-2.2.3'
implementation 'org.greenrobot:eventbus:3.3.1'

implementation 'com.google.protobuf:protobuf-javalite:4.30.2'
implementation('com.google.protobuf:protobuf-kotlin:4.30.2') {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'

implementation 'com.github.iammohdzaki:Password-Generator:0.6'
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
package io.openremote.orlib.service

import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.core.app.ActivityCompat
import io.openremote.orlib.R
import io.openremote.orlib.service.BleProvider.BleCallback
import io.openremote.orlib.service.BleProvider.Companion.BLUETOOTH_PERMISSION_REQUEST_CODE
import io.openremote.orlib.service.BleProvider.Companion.ENABLE_BLUETOOTH_REQUEST_CODE
import io.openremote.orlib.service.espprovision.BatteryProvision
import io.openremote.orlib.service.espprovision.CallbackChannel
import io.openremote.orlib.service.espprovision.DeviceConnection
import io.openremote.orlib.service.espprovision.DeviceRegistry
import io.openremote.orlib.service.espprovision.WifiProvisioner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.URL

object ESPProvisionProviderActions {
const val PROVIDER_INIT = "PROVIDER_INIT"
const val PROVIDER_ENABLE = "PROVIDER_ENABLE"
const val PROVIDER_DISABLE = "PROVIDER_DISABLE"
const val START_BLE_SCAN = "START_BLE_SCAN"
const val STOP_BLE_SCAN = "STOP_BLE_SCAN"
const val CONNECT_TO_DEVICE = "CONNECT_TO_DEVICE"
const val DISCONNECT_FROM_DEVICE = "DISCONNECT_FROM_DEVICE"
const val START_WIFI_SCAN = "START_WIFI_SCAN"
const val STOP_WIFI_SCAN = "STOP_WIFI_SCAN"
const val SEND_WIFI_CONFIGURATION = "SEND_WIFI_CONFIGURATION"
const val PROVISION_DEVICE = "PROVISION_DEVICE"
const val EXIT_PROVISIONING = "EXIT_PROVISIONING"
}

class ESPProvisionProvider(val context: Context, val apiURL: URL = URL("http://localhost:8080/api/master")) {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val deviceRegistry: DeviceRegistry
var deviceConnection: DeviceConnection? = null

private var searchDeviceTimeout: Long = 120
private var searchDeviceMaxIterations = 25

var wifiProvisioner: WifiProvisioner? = null
private var searchWifiTimeout: Long = 120
private var searchWifiMaxIterations = 25

init {
deviceRegistry = DeviceRegistry(context, searchDeviceTimeout, searchDeviceMaxIterations)
}

interface ESPProvisionCallback {
fun accept(responseData: Map<String, Any>)
}

companion object {
private const val espProvisionDisabledKey = "espProvisionDisabled"
private const val version = "beta"

const val TAG = "ESPProvisionProvider"

const val ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE = 655
const val BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE = 656
}

private val bluetoothAdapter: BluetoothAdapter by lazy {
val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}

fun initialize(): Map<String, Any> {
val sharedPreferences =
context.getSharedPreferences(context.getString(R.string.app_name), Context.MODE_PRIVATE)

return hashMapOf(
"action" to ESPProvisionProviderActions.PROVIDER_INIT,
"provider" to "espprovision",
"version" to version,
"requiresPermission" to true,
"hasPermission" to hasPermission(),
"success" to true,
"enabled" to false,
"disabled" to sharedPreferences.contains(espProvisionDisabledKey)
)
}

@SuppressLint("MissingPermission")
fun enable(callback: ESPProvisionCallback, activity: Activity) {
deviceRegistry.callbackChannel = CallbackChannel(callback, "espprovision")
deviceRegistry.enable()

if (!bluetoothAdapter.isEnabled) {
Log.d("ESP", "BLE not enabled")
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activity.startActivityForResult(enableBtIntent,
ESPProvisionProvider.Companion.ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE
)
} else if (!hasPermission()) {
Log.d("ESP", "Does not have permissions")
requestPermissions(activity)
}


if (bluetoothAdapter.isEnabled && hasPermission()) {
providerEnabled(deviceRegistry.callbackChannel)
}
}

fun providerEnabled(callbackChannel: CallbackChannel?) {
val sharedPreferences =
context.getSharedPreferences(
context.getString(R.string.app_name),
Context.MODE_PRIVATE
)

sharedPreferences.edit()
.remove(espProvisionDisabledKey)
.apply()

callbackChannel?.sendMessage(ESPProvisionProviderActions.PROVIDER_ENABLE,
hashMapOf(
"hasPermission" to hasPermission(),
"success" to true,
"enabled" to true,
"disabled" to sharedPreferences.contains(espProvisionDisabledKey)
)
)
}

@SuppressLint("MissingPermission")
fun disable(): Map<String, Any> {
deviceRegistry.disable()

// disconnectFromDevice()

val sharedPreferences =
context.getSharedPreferences(context.getString(R.string.app_name), Context.MODE_PRIVATE)
sharedPreferences.edit()
.putBoolean(espProvisionDisabledKey, true)
.apply()

return hashMapOf(
"action" to ESPProvisionProviderActions.PROVIDER_DISABLE,
"provider" to "espprovision"
)
}

@SuppressLint("MissingPermission")
fun onRequestPermissionsResult(
activity: Activity,
requestCode: Int,
prefix: String?
) {
Log.d("espprovision", "onRequestPermissionsResult called with prefix >" + prefix + "<")
if (requestCode == BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE) {
val hasPermission = hasPermission()
if (hasPermission) {
if (!bluetoothAdapter.isEnabled) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activity.startActivityForResult(enableBtIntent, ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE)
} else {
providerEnabled(deviceRegistry.callbackChannel)
if (prefix != null) {
deviceRegistry.startDevicesScan(prefix)
}
}
}
} else if (requestCode == ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE) {
if (bluetoothAdapter.isEnabled) {
providerEnabled(deviceRegistry.callbackChannel)
if (prefix != null) {
deviceRegistry.startDevicesScan(prefix)
}
}
}
}

// Device scan

@SuppressLint("MissingPermission")
fun startDevicesScan(prefix: String?, activity: Activity, callback: ESPProvisionCallback) {
deviceRegistry.callbackChannel = CallbackChannel(callback, "espprovision")
if (!bluetoothAdapter.isEnabled) {
Log.d("ESP", "BLE not enabled")
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activity.startActivityForResult(enableBtIntent,
ESPProvisionProvider.Companion.ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE
)
} else if (!hasPermission()) {
Log.d("ESP", "Does not have permissions")
requestPermissions(activity)
} else {
deviceRegistry.startDevicesScan(prefix)
}
}

@SuppressLint("MissingPermission")
fun stopDevicesScan() {
deviceRegistry.stopDevicesScan()
}

// MARK: Device connect/disconnect

@SuppressLint("MissingPermission")
fun connectTo(deviceId: String, pop: String? = null, username: String? = null) {
if (deviceConnection == null) {
deviceConnection = DeviceConnection(deviceRegistry, deviceRegistry.callbackChannel)
}
deviceConnection?.connectTo(deviceId, pop, username)
}

fun disconnectFromDevice() {
wifiProvisioner?.stopWifiScan()
deviceConnection?.disconnectFromDevice()
}

fun exitProvisioning() {
if (deviceConnection == null) {
return
}
if (!deviceConnection!!.isConnected) {
sendExitProvisioningError(ESPProviderErrorCode.NOT_CONNECTED, "No connection established to device")
return
}
deviceConnection!!.exitProvisioning()
deviceRegistry?.callbackChannel?.sendMessage(
ESPProvisionProviderActions.EXIT_PROVISIONING,
mapOf("exit" to true)
)
}

private fun sendExitProvisioningError(error: ESPProviderErrorCode, errorMessage: String?) {
val data = mutableMapOf<String, Any>()

data["exit"] = false
data["errorCode"] = error.code
errorMessage?.let {
data["errorMessage"] = it
}

deviceRegistry?.callbackChannel?.sendMessage(ESPProvisionProviderActions.EXIT_PROVISIONING, data)
}

// Wifi scan

fun startWifiScan() {
if (wifiProvisioner == null) {
wifiProvisioner = WifiProvisioner(deviceConnection, deviceRegistry.callbackChannel, searchWifiTimeout, searchWifiMaxIterations)
}
wifiProvisioner!!.startWifiScan()
}

fun stopWifiScan() {
wifiProvisioner?.stopWifiScan()
}

fun sendWifiConfiguration(ssid: String, password: String) {
if (wifiProvisioner == null) {
wifiProvisioner = WifiProvisioner(deviceConnection, deviceRegistry.callbackChannel, searchWifiTimeout, searchWifiMaxIterations)
}
wifiProvisioner!!.sendWifiConfiguration(ssid, password)
}

// OR Configuration

fun provisionDevice(userToken: String) {
val batteryProvision = BatteryProvision(deviceConnection, deviceRegistry.callbackChannel, apiURL)
CoroutineScope(Dispatchers.IO).launch {
batteryProvision.provision(userToken)
}
}

private fun requestPermissions(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.requestPermissions(
activity,
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
),
ESPProvisionProvider.Companion.BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE
)
} else {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
ESPProvisionProvider.Companion.BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE
)
}
}

private fun hasPermission() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
} else {
context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}

}

data class ESPProviderException(val errorCode: ESPProviderErrorCode, val errorMessage: String) : Exception()

enum class ESPProviderErrorCode(val code: Int) {
UNKNOWN_DEVICE(100),

BLE_COMMUNICATION_ERROR(200),

NOT_CONNECTED(300),
COMMUNICATION_ERROR(301),

SECURITY_ERROR(400),

WIFI_CONFIGURATION_ERROR(500),
WIFI_COMMUNICATION_ERROR(501),
WIFI_AUTHENTICATION_ERROR(502),
WIFI_NETWORK_NOT_FOUND(503),

TIMEOUT_ERROR(600),

GENERIC_ERROR(10000);
}
Loading
Loading