Skip to content

Commit 4310a46

Browse files
authored
Feature/esp provision provider (#39)
* WIP for ESPProvisionProvider: device and wifi discovery happy flow working, lots of cleanup to do * Implement device disconnect and better handling of connection statuses * Whole ESPProvision flow working but code review/cleaning required * Error handling in case of missing keys from webapp messages * Handle error during Wifi scan * Some better error handling * M1V1-55: Properly handle list of discovered network, avoiding duplicates, taking changing RSSI into account and continuing scan until explicitly stopped. * M1V1-61: on permissions granted, only start a scan if prefix is set (= when scan has been asked explicitly) * M1V1-64: Add indication whether exit provisioning was successful or not * Improve check for BLE permissions and only report back to webapp when permissions have been requested to user (if required) (GitHub issue #40) * Make function suspend to not block the main thread * Comment on usage of Main context * Clean-up comments * Don't need full package prefix * Some improvements on coroutine contexts usage * Properly pass pop received from web app to provider * Make provisioning generic, using device API instead of battery one * ORConfigChannelProtocol class is now generated from Protobuf spec as part of build * Trigger Build
1 parent 1d615b4 commit 4310a46

15 files changed

Lines changed: 1561 additions & 0 deletions

File tree

ORLib/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ dependencies {
6464
implementation platform('com.google.firebase:firebase-bom:33.12.0')
6565
implementation 'com.google.firebase:firebase-messaging-ktx'
6666
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
67+
implementation 'com.github.espressif:esp-idf-provisioning-android:lib-2.2.3'
68+
implementation 'org.greenrobot:eventbus:3.3.1'
69+
70+
implementation 'com.google.protobuf:protobuf-javalite:4.33.2'
71+
implementation('com.google.protobuf:protobuf-kotlin:4.33.2') {
72+
exclude group: 'com.google.protobuf', module: 'protobuf-java'
73+
}
74+
75+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
76+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
77+
78+
implementation 'com.github.iammohdzaki:Password-Generator:0.6'
79+
80+
api project(':protobuf')
6781
}
6882

6983

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
package io.openremote.orlib.service
2+
3+
import android.Manifest
4+
import android.annotation.SuppressLint
5+
import android.app.Activity
6+
import android.bluetooth.BluetoothAdapter
7+
import android.bluetooth.BluetoothManager
8+
import android.content.Context
9+
import android.content.Intent
10+
import android.content.pm.PackageManager
11+
import android.os.Build
12+
import android.util.Log
13+
import androidx.annotation.VisibleForTesting
14+
import androidx.core.app.ActivityCompat
15+
import io.openremote.orlib.R
16+
import io.openremote.orlib.service.espprovision.CallbackChannel
17+
import io.openremote.orlib.service.espprovision.DeviceConnection
18+
import io.openremote.orlib.service.espprovision.DeviceProvision
19+
import io.openremote.orlib.service.espprovision.DeviceRegistry
20+
import io.openremote.orlib.service.espprovision.WifiProvisioner
21+
import kotlinx.coroutines.CoroutineScope
22+
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.launch
24+
import java.net.URL
25+
26+
object ESPProvisionProviderActions {
27+
const val PROVIDER_INIT = "PROVIDER_INIT"
28+
const val PROVIDER_ENABLE = "PROVIDER_ENABLE"
29+
const val PROVIDER_DISABLE = "PROVIDER_DISABLE"
30+
const val START_BLE_SCAN = "START_BLE_SCAN"
31+
const val STOP_BLE_SCAN = "STOP_BLE_SCAN"
32+
const val CONNECT_TO_DEVICE = "CONNECT_TO_DEVICE"
33+
const val DISCONNECT_FROM_DEVICE = "DISCONNECT_FROM_DEVICE"
34+
const val START_WIFI_SCAN = "START_WIFI_SCAN"
35+
const val STOP_WIFI_SCAN = "STOP_WIFI_SCAN"
36+
const val SEND_WIFI_CONFIGURATION = "SEND_WIFI_CONFIGURATION"
37+
const val PROVISION_DEVICE = "PROVISION_DEVICE"
38+
const val EXIT_PROVISIONING = "EXIT_PROVISIONING"
39+
}
40+
41+
class ESPProvisionProvider(val context: Context, val apiURL: URL = URL("http://localhost:8080/api/master")) {
42+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
43+
val deviceRegistry: DeviceRegistry
44+
var deviceConnection: DeviceConnection? = null
45+
46+
private var searchDeviceTimeout: Long = 120
47+
private var searchDeviceMaxIterations = 25
48+
49+
var wifiProvisioner: WifiProvisioner? = null
50+
private var searchWifiTimeout: Long = 120
51+
private var searchWifiMaxIterations = 25
52+
53+
init {
54+
deviceRegistry = DeviceRegistry(context, searchDeviceTimeout, searchDeviceMaxIterations)
55+
}
56+
57+
interface ESPProvisionCallback {
58+
fun accept(responseData: Map<String, Any>)
59+
}
60+
61+
companion object {
62+
private const val espProvisionDisabledKey = "espProvisionDisabled"
63+
private const val version = "beta"
64+
65+
const val TAG = "ESPProvisionProvider"
66+
67+
const val ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE = 655
68+
const val BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE = 656
69+
}
70+
71+
private val bluetoothAdapter: BluetoothAdapter by lazy {
72+
val bluetoothManager =
73+
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
74+
bluetoothManager.adapter
75+
}
76+
77+
fun initialize(): Map<String, Any> {
78+
val sharedPreferences =
79+
context.getSharedPreferences(context.getString(R.string.app_name), Context.MODE_PRIVATE)
80+
81+
return hashMapOf(
82+
"action" to ESPProvisionProviderActions.PROVIDER_INIT,
83+
"provider" to "espprovision",
84+
"version" to version,
85+
"requiresPermission" to true,
86+
"hasPermission" to hasPermission(),
87+
"success" to true,
88+
"enabled" to false,
89+
"disabled" to sharedPreferences.contains(espProvisionDisabledKey)
90+
)
91+
}
92+
93+
@SuppressLint("MissingPermission")
94+
fun enable(callback: ESPProvisionCallback, activity: Activity) {
95+
deviceRegistry.callbackChannel = CallbackChannel(callback, "espprovision")
96+
deviceRegistry.enable()
97+
98+
if (!bluetoothAdapter.isEnabled) {
99+
Log.d("ESP", "BLE not enabled")
100+
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
101+
activity.startActivityForResult(enableBtIntent,
102+
ESPProvisionProvider.Companion.ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE
103+
)
104+
} else if (!hasPermission()) {
105+
Log.d("ESP", "Does not have permissions")
106+
requestPermissions(activity)
107+
}
108+
109+
110+
if (bluetoothAdapter.isEnabled && hasPermission()) {
111+
providerEnabled(deviceRegistry.callbackChannel)
112+
}
113+
}
114+
115+
fun providerEnabled(callbackChannel: CallbackChannel?) {
116+
val sharedPreferences =
117+
context.getSharedPreferences(
118+
context.getString(R.string.app_name),
119+
Context.MODE_PRIVATE
120+
)
121+
122+
sharedPreferences.edit()
123+
.remove(espProvisionDisabledKey)
124+
.apply()
125+
126+
callbackChannel?.sendMessage(ESPProvisionProviderActions.PROVIDER_ENABLE,
127+
hashMapOf(
128+
"hasPermission" to hasPermission(),
129+
"success" to true,
130+
"enabled" to true,
131+
"disabled" to sharedPreferences.contains(espProvisionDisabledKey)
132+
)
133+
)
134+
}
135+
136+
@SuppressLint("MissingPermission")
137+
fun disable(): Map<String, Any> {
138+
deviceRegistry.disable()
139+
140+
// disconnectFromDevice()
141+
142+
val sharedPreferences =
143+
context.getSharedPreferences(context.getString(R.string.app_name), Context.MODE_PRIVATE)
144+
sharedPreferences.edit()
145+
.putBoolean(espProvisionDisabledKey, true)
146+
.apply()
147+
148+
return hashMapOf(
149+
"action" to ESPProvisionProviderActions.PROVIDER_DISABLE,
150+
"provider" to "espprovision"
151+
)
152+
}
153+
154+
@SuppressLint("MissingPermission")
155+
fun onRequestPermissionsResult(
156+
activity: Activity,
157+
requestCode: Int,
158+
prefix: String?
159+
) {
160+
Log.d("espprovision", "onRequestPermissionsResult called with prefix >" + prefix + "<")
161+
if (requestCode == BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE) {
162+
val hasPermission = hasPermission()
163+
if (hasPermission) {
164+
if (!bluetoothAdapter.isEnabled) {
165+
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
166+
activity.startActivityForResult(enableBtIntent, ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE)
167+
} else {
168+
providerEnabled(deviceRegistry.callbackChannel)
169+
if (prefix != null) {
170+
deviceRegistry.startDevicesScan(prefix)
171+
}
172+
}
173+
}
174+
} else if (requestCode == ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE) {
175+
if (bluetoothAdapter.isEnabled) {
176+
providerEnabled(deviceRegistry.callbackChannel)
177+
if (prefix != null) {
178+
deviceRegistry.startDevicesScan(prefix)
179+
}
180+
}
181+
}
182+
}
183+
184+
// Device scan
185+
186+
@SuppressLint("MissingPermission")
187+
fun startDevicesScan(prefix: String?, activity: Activity, callback: ESPProvisionCallback) {
188+
deviceRegistry.callbackChannel = CallbackChannel(callback, "espprovision")
189+
if (!bluetoothAdapter.isEnabled) {
190+
Log.d("ESP", "BLE not enabled")
191+
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
192+
activity.startActivityForResult(enableBtIntent,
193+
ESPProvisionProvider.Companion.ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE
194+
)
195+
} else if (!hasPermission()) {
196+
Log.d("ESP", "Does not have permissions")
197+
requestPermissions(activity)
198+
} else {
199+
deviceRegistry.startDevicesScan(prefix)
200+
}
201+
}
202+
203+
@SuppressLint("MissingPermission")
204+
fun stopDevicesScan() {
205+
deviceRegistry.stopDevicesScan()
206+
}
207+
208+
// MARK: Device connect/disconnect
209+
210+
@SuppressLint("MissingPermission")
211+
fun connectTo(deviceId: String, pop: String? = null, username: String? = null) {
212+
if (deviceConnection == null) {
213+
deviceConnection = DeviceConnection(deviceRegistry, deviceRegistry.callbackChannel)
214+
}
215+
deviceConnection?.connectTo(deviceId, pop, username)
216+
}
217+
218+
fun disconnectFromDevice() {
219+
wifiProvisioner?.stopWifiScan()
220+
deviceConnection?.disconnectFromDevice()
221+
}
222+
223+
fun exitProvisioning() {
224+
if (deviceConnection == null) {
225+
return
226+
}
227+
if (!deviceConnection!!.isConnected) {
228+
sendExitProvisioningError(ESPProviderErrorCode.NOT_CONNECTED, "No connection established to device")
229+
return
230+
}
231+
deviceConnection!!.exitProvisioning()
232+
deviceRegistry?.callbackChannel?.sendMessage(
233+
ESPProvisionProviderActions.EXIT_PROVISIONING,
234+
mapOf("exit" to true)
235+
)
236+
}
237+
238+
private fun sendExitProvisioningError(error: ESPProviderErrorCode, errorMessage: String?) {
239+
val data = mutableMapOf<String, Any>()
240+
241+
data["exit"] = false
242+
data["errorCode"] = error.code
243+
errorMessage?.let {
244+
data["errorMessage"] = it
245+
}
246+
247+
deviceRegistry?.callbackChannel?.sendMessage(ESPProvisionProviderActions.EXIT_PROVISIONING, data)
248+
}
249+
250+
// Wifi scan
251+
252+
fun startWifiScan() {
253+
if (wifiProvisioner == null) {
254+
wifiProvisioner = WifiProvisioner(deviceConnection, deviceRegistry.callbackChannel, searchWifiTimeout, searchWifiMaxIterations)
255+
}
256+
wifiProvisioner!!.startWifiScan()
257+
}
258+
259+
fun stopWifiScan() {
260+
wifiProvisioner?.stopWifiScan()
261+
}
262+
263+
fun sendWifiConfiguration(ssid: String, password: String) {
264+
if (wifiProvisioner == null) {
265+
wifiProvisioner = WifiProvisioner(deviceConnection, deviceRegistry.callbackChannel, searchWifiTimeout, searchWifiMaxIterations)
266+
}
267+
wifiProvisioner!!.sendWifiConfiguration(ssid, password)
268+
}
269+
270+
// OR Configuration
271+
272+
fun provisionDevice(userToken: String) {
273+
val deviceProvision = DeviceProvision(deviceConnection, deviceRegistry.callbackChannel, apiURL)
274+
CoroutineScope(Dispatchers.IO).launch {
275+
deviceProvision.provision(userToken)
276+
}
277+
}
278+
279+
private fun requestPermissions(activity: Activity) {
280+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
281+
ActivityCompat.requestPermissions(
282+
activity,
283+
arrayOf(
284+
Manifest.permission.BLUETOOTH_SCAN,
285+
Manifest.permission.BLUETOOTH_CONNECT
286+
),
287+
ESPProvisionProvider.Companion.BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE
288+
)
289+
} else {
290+
ActivityCompat.requestPermissions(
291+
activity,
292+
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
293+
ESPProvisionProvider.Companion.BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE
294+
)
295+
}
296+
}
297+
298+
private fun hasPermission() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
299+
context.checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
300+
context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
301+
} else {
302+
context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
303+
}
304+
305+
}
306+
307+
data class ESPProviderException(val errorCode: ESPProviderErrorCode, val errorMessage: String) : Exception()
308+
309+
enum class ESPProviderErrorCode(val code: Int) {
310+
UNKNOWN_DEVICE(100),
311+
312+
BLE_COMMUNICATION_ERROR(200),
313+
314+
NOT_CONNECTED(300),
315+
COMMUNICATION_ERROR(301),
316+
317+
SECURITY_ERROR(400),
318+
319+
WIFI_CONFIGURATION_ERROR(500),
320+
WIFI_COMMUNICATION_ERROR(501),
321+
WIFI_AUTHENTICATION_ERROR(502),
322+
WIFI_NETWORK_NOT_FOUND(503),
323+
324+
TIMEOUT_ERROR(600),
325+
326+
GENERIC_ERROR(10000);
327+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.openremote.orlib.service.espprovision
2+
3+
import io.openremote.orlib.service.ESPProvisionProvider
4+
5+
class CallbackChannel(private val espProvisionCallback: ESPProvisionProvider.ESPProvisionCallback, private val provider: String) {
6+
7+
fun sendMessage(action: String, data: Map<String, Any>? = null) {
8+
var payload: MutableMap<String, Any> = hashMapOf(
9+
"action" to action,
10+
"provider" to "espprovision")
11+
12+
data?.let { payload.putAll(it) }
13+
14+
espProvisionCallback.accept(payload)
15+
}
16+
}

0 commit comments

Comments
 (0)