react-native-gizwits-sdk-v5 1.7.7-beta → 1.7.9-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,10 +67,10 @@ dependencies {
67
67
  // ESPTouch
68
68
  implementation 'com.github.EspressifApp:lib-esptouch-android:1.1.1'
69
69
 
70
- implementation("io.github.gizwits:sdk:1.4.10")
71
- implementation("io.github.gizwits:sdk-bluetooth:1.4.10")
72
- implementation("io.github.gizwits:sdk-lan:1.4.10")
73
- implementation("io.github.gizwits:sdk-mqtt:1.4.10")
70
+ implementation("io.github.gizwits:sdk:1.4.11")
71
+ implementation("io.github.gizwits:sdk-bluetooth:1.4.11")
72
+ implementation("io.github.gizwits:sdk-lan:1.4.11")
73
+ implementation("io.github.gizwits:sdk-mqtt:1.4.11")
74
74
  // implementation files('libs/sdk-release.aar', 'libs/sdk-bluetooth-release.aar', 'libs/sdk-lan-release.aar', 'libs/sdk-mqtt-release.aar')
75
75
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutine_version")
76
76
  // retrofit
@@ -82,4 +82,7 @@ dependencies {
82
82
  //ktor
83
83
  implementation "io.ktor:ktor-network:$ktor_network_version"
84
84
 
85
+ testImplementation 'junit:junit:4.13.2'
86
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutine_version"
87
+
85
88
  }
@@ -0,0 +1,72 @@
1
+ package com.gizwits.reactnativegizwitssdkv5
2
+
3
+ import kotlinx.coroutines.CoroutineScope
4
+ import kotlinx.coroutines.Job
5
+ import kotlinx.coroutines.channels.Channel
6
+ import kotlinx.coroutines.delay
7
+ import kotlinx.coroutines.launch
8
+
9
+ internal class ConflatedEventDispatcher(
10
+ scope: CoroutineScope,
11
+ private val windowMillis: Long,
12
+ private val dispatch: suspend () -> Unit,
13
+ ) {
14
+ private val requests = Channel<Unit>(Channel.CONFLATED)
15
+ private val worker = scope.launch {
16
+ for (request in requests) {
17
+ if (windowMillis > 0) {
18
+ delay(windowMillis)
19
+ }
20
+ while (requests.tryReceive().isSuccess) {
21
+ // Keep only the latest-state notification for this window.
22
+ }
23
+ dispatch()
24
+ }
25
+ }
26
+
27
+ init {
28
+ require(windowMillis >= 0) { "windowMillis must not be negative" }
29
+ }
30
+
31
+ internal fun request(): Boolean = requests.trySend(Unit).isSuccess
32
+
33
+ internal fun cancel() {
34
+ requests.close()
35
+ worker.cancel()
36
+ }
37
+ }
38
+
39
+ internal class KeyedSubscriptionRegistry<K, V>(
40
+ private val keySelector: (V) -> K,
41
+ private val subscribe: (V) -> Job,
42
+ ) {
43
+ private data class Subscription<V>(val value: V, val job: Job)
44
+
45
+ private val subscriptions = linkedMapOf<K, Subscription<V>>()
46
+
47
+ @Synchronized
48
+ internal fun update(values: Collection<V>) {
49
+ val valuesByKey = values.associateBy(keySelector)
50
+
51
+ (subscriptions.keys - valuesByKey.keys).forEach { key ->
52
+ subscriptions.remove(key)?.job?.cancel()
53
+ }
54
+
55
+ valuesByKey.forEach { (key, value) ->
56
+ val existing = subscriptions[key]
57
+ if (existing == null || !existing.job.isActive || existing.value !== value) {
58
+ existing?.job?.cancel()
59
+ subscriptions[key] = Subscription(value, subscribe(value))
60
+ }
61
+ }
62
+ }
63
+
64
+ @Synchronized
65
+ internal fun cancelAll() {
66
+ subscriptions.values.forEach { it.job.cancel() }
67
+ subscriptions.clear()
68
+ }
69
+
70
+ @Synchronized
71
+ internal fun keys(): Set<K> = subscriptions.keys.toSet()
72
+ }
@@ -28,20 +28,18 @@ import com.google.gson.annotations.SerializedName
28
28
  import kotlinx.coroutines.CoroutineScope
29
29
  import kotlinx.coroutines.Dispatchers
30
30
  import kotlinx.coroutines.ExperimentalCoroutinesApi
31
- import kotlinx.coroutines.async
32
- import kotlinx.coroutines.coroutineScope
31
+ import kotlinx.coroutines.SupervisorJob
32
+ import kotlinx.coroutines.cancel
33
33
  import kotlinx.coroutines.flow.Flow
34
- import kotlinx.coroutines.flow.collectLatest
34
+ import kotlinx.coroutines.flow.collect
35
35
  import kotlinx.coroutines.flow.combine
36
- import kotlinx.coroutines.flow.debounce
37
- import kotlinx.coroutines.flow.emptyFlow
38
- import kotlinx.coroutines.flow.first
39
- import kotlinx.coroutines.flow.flatMapConcat
40
- import kotlinx.coroutines.flow.last
41
- import kotlinx.coroutines.flow.launchIn
36
+ import kotlinx.coroutines.flow.distinctUntilChanged
37
+ import kotlinx.coroutines.flow.drop
38
+ import kotlinx.coroutines.flow.filterNotNull
42
39
  import kotlinx.coroutines.flow.mapLatest
43
40
  import kotlinx.coroutines.flow.take
44
41
  import kotlinx.coroutines.launch
42
+ import kotlinx.coroutines.supervisorScope
45
43
  import org.json.JSONArray
46
44
  import org.json.JSONObject
47
45
 
@@ -62,15 +60,24 @@ data class GizFeedback(
62
60
 
63
61
  class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
64
62
 
63
+ private val moduleJob = SupervisorJob()
64
+ private val moduleScope = CoroutineScope(moduleJob + Dispatchers.IO)
65
+ @Volatile
66
+ private var latestDeviceList = emptyList<GizDevice>()
65
67
  private var mReactContext: ReactContext? = null
68
+ private val deviceListEventDispatcher = ConflatedEventDispatcher(
69
+ scope = moduleScope,
70
+ windowMillis = 500,
71
+ dispatch = ::emitDeviceListChange,
72
+ )
73
+ private val deviceSubscriptions = KeyedSubscriptionRegistry<String, GizDevice>(
74
+ keySelector = GizDevice::id,
75
+ subscribe = ::subscribeToDevice,
76
+ )
66
77
 
67
- private var isSubscriptDevices: List<String> = listOf();
68
78
  init {
69
79
  mReactContext = reactContext
70
80
  }
71
- companion object {
72
- internal var devices = listOf<GizDevice>()
73
- }
74
81
  enum class EventName(val value: String) {
75
82
  DeviceDataListener("DeviceDataListener"),
76
83
  DeviceListListener("DeviceListListener"),
@@ -120,7 +127,7 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
120
127
 
121
128
  override fun initialize() {
122
129
  super.initialize()
123
- CoroutineScope(Dispatchers.IO).launch {
130
+ moduleScope.launch {
124
131
  GizSDKManager.subscribeBindEvent().collect {
125
132
  val data = JSONObject()
126
133
  data.put("did", it.first)
@@ -129,159 +136,141 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
129
136
  }
130
137
  }
131
138
 
132
- // 监听设备对象
133
- deviceListState
134
- .debounce(500)
135
- .flatMapConcat { deviceList ->
136
- deviceList.map { dev ->
137
-
138
- if (!isSubscriptDevices.contains(dev.mac)) {
139
- CoroutineScope(Dispatchers.IO).launch {
140
- async {
141
- dev.bleCapability.subscribeModuleProfile().debounce(500).collect {
142
- if (it != null) {
143
- emitDeviceListChange()
144
- }
145
- }
146
- }
147
- async {
148
- combine(
149
- dev.bleCapability.subscribeStatus(),
150
- dev.bleCapability.subscribeIsLogin(),
151
- ) { status, isLogin ->
152
- Pair(status, isLogin)
153
- }.collect {
154
- val data = JSONObject()
155
- data.put("state", it.first.toInt())
156
- data.put("isLogin", it.second)
157
- data.put("type", "BLE")
158
- data.put("device", dev.toJsonObject())
159
- sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
160
- }
161
- }
162
- async {
163
- dev.bleCapability.subscribeDp().collect{
164
- val data = JSONObject()
165
- data.put("data", JSONObject(it.toString()))
166
- data.put("type", "BLE")
167
- data.put("device", dev.toJsonObject())
168
- sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
169
- }
170
- }
171
- async {
172
- dev.lanCapability.subscribeModuleProfile().debounce(500).collectLatest{
173
- if (it != null) {
174
- emitDeviceListChange()
175
- }
176
- }
177
- }
178
-
179
- async {
180
- combineLatest(
181
- dev.lanCapability.subscribeStatus(),
182
- dev.lanCapability.subscribeIsLogin(),
183
- ) { status, isLogin ->
184
- Pair(status, isLogin)
185
- }.collectLatest {
186
- val data = JSONObject()
187
- data.put("state", it.first.toInt())
188
- data.put("isLogin", it.second)
189
- data.put("type", "LAN")
190
- data.put("device", dev.toJsonObject())
191
- sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
192
- }
193
- }
194
-
195
- async {
196
- dev.lanCapability.subscribeDp().collect{
197
- val data = JSONObject()
198
- data.put("data", JSONObject(it.toString()))
199
- data.put("type", "LAN")
200
- data.put("device", dev.toJsonObject())
201
- sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
202
- }
203
- }
204
-
205
- async {
206
- combineLatest(
207
- dev.mqttCapability.subscribeStatus(),
208
- dev.mqttCapability.subscribeIsLogin(),
209
- ) { status, isLogin ->
210
- Pair(status, isLogin)
211
- }.collectLatest {
212
- val data = JSONObject()
213
- data.put("state", it.first.toInt())
214
- data.put("isLogin", it.second)
215
- data.put("type", "MQTT")
216
- data.put("device", dev.toJsonObject())
217
- sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
218
- }
219
- }
220
-
221
- // 不会主动发生改表
222
- // async {
223
- // dev.mqttCapability.subscribeModuleProfile().debounce(500).collectLatest{
224
- // if (it != null) {
225
- // emitDeviceListChange()
226
- // }
227
- // }
228
- // }
229
-
230
- async {
231
- dev.mqttCapability.subscribeDp().collect{
232
- val data = JSONObject()
233
- data.put("data", JSONObject(it.toString()))
234
- data.put("type", "MQTT")
235
- data.put("device", dev.toJsonObject())
236
- sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
237
- }
238
- }
239
- }
139
+ moduleScope.launch {
140
+ deviceListState
141
+ .distinctUntilChanged { previous, current ->
142
+ previous.size == current.size && previous.indices.all { index ->
143
+ previous[index].id == current[index].id && previous[index] === current[index]
240
144
  }
241
145
  }
242
-
243
- /*
244
- 更新当前已经订阅的设备
245
- 如果设备消失,又出现,就可以正常的重新订阅
246
- */
247
- isSubscriptDevices = deviceList.map{
248
- it.mac
146
+ .collect { deviceList ->
147
+ latestDeviceList = deviceList
148
+ deviceSubscriptions.update(deviceList)
149
+ requestDeviceListChange()
249
150
  }
151
+ }
152
+ }
250
153
 
251
- emptyFlow<Unit>()
154
+ private fun subscribeToDevice(device: GizDevice) = moduleScope.launch {
155
+ supervisorScope {
156
+ launch {
157
+ device.bleCapability.subscribeModuleProfile()
158
+ .drop(1)
159
+ .filterNotNull()
160
+ .collect { requestDeviceListChange() }
252
161
  }
253
- .launchIn(CoroutineScope(Dispatchers.IO))
254
-
255
- // 推送设备列表变化
256
- CoroutineScope(Dispatchers.IO).launch {
257
- deviceListState.debounce(500).collectLatest { deviceList ->
258
- devices = deviceList
259
- emitDeviceListChange()
162
+ launch {
163
+ combine(
164
+ device.bleCapability.subscribeStatus(),
165
+ device.bleCapability.subscribeIsLogin(),
166
+ ) { status, isLogin -> Pair(status, isLogin) }
167
+ .collect { (status, isLogin) ->
168
+ val data = JSONObject()
169
+ data.put("state", status.toInt())
170
+ data.put("isLogin", isLogin)
171
+ data.put("type", "BLE")
172
+ data.put("device", device.toJsonObject())
173
+ sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
174
+ }
175
+ }
176
+ launch {
177
+ device.bleCapability.subscribeDp().collect { payload ->
178
+ val data = JSONObject()
179
+ data.put("data", JSONObject(payload.toString()))
180
+ data.put("type", "BLE")
181
+ data.put("device", device.toJsonObject())
182
+ sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
183
+ }
184
+ }
185
+ launch {
186
+ device.lanCapability.subscribeModuleProfile()
187
+ .drop(1)
188
+ .filterNotNull()
189
+ .collect { requestDeviceListChange() }
190
+ }
191
+ launch {
192
+ combineLatest(
193
+ device.lanCapability.subscribeStatus(),
194
+ device.lanCapability.subscribeIsLogin(),
195
+ ) { status, isLogin -> Pair(status, isLogin) }
196
+ .collect { (status, isLogin) ->
197
+ val data = JSONObject()
198
+ data.put("state", status.toInt())
199
+ data.put("isLogin", isLogin)
200
+ data.put("type", "LAN")
201
+ data.put("device", device.toJsonObject())
202
+ sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
203
+ }
204
+ }
205
+ launch {
206
+ device.lanCapability.subscribeDp().collect { payload ->
207
+ val data = JSONObject()
208
+ data.put("data", JSONObject(payload.toString()))
209
+ data.put("type", "LAN")
210
+ data.put("device", device.toJsonObject())
211
+ sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
212
+ }
213
+ }
214
+ launch {
215
+ combineLatest(
216
+ device.mqttCapability.subscribeStatus(),
217
+ device.mqttCapability.subscribeIsLogin(),
218
+ ) { status, isLogin -> Pair(status, isLogin) }
219
+ .collect { (status, isLogin) ->
220
+ val data = JSONObject()
221
+ data.put("state", status.toInt())
222
+ data.put("isLogin", isLogin)
223
+ data.put("type", "MQTT")
224
+ data.put("device", device.toJsonObject())
225
+ sendEvent(EventName.DeviceStateListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
226
+ }
227
+ }
228
+ launch {
229
+ device.mqttCapability.subscribeDp().collect { payload ->
230
+ val data = JSONObject()
231
+ data.put("data", JSONObject(payload.toString()))
232
+ data.put("type", "MQTT")
233
+ data.put("device", device.toJsonObject())
234
+ sendEvent(EventName.DeviceDataListener.name, GizRNCallbackManager.jsonObject2WriteableMap(data))
235
+ }
260
236
  }
261
237
  }
262
238
  }
263
239
 
240
+ private fun requestDeviceListChange() {
241
+ deviceListEventDispatcher.request()
242
+ }
243
+
264
244
  /**
265
245
  * RN的场景下
266
246
  * module profile 变更 相当于设备列表变更
267
247
  */
268
- suspend fun emitDeviceListChange() {
269
- val deviceList = deviceListState.first()
248
+ private suspend fun emitDeviceListChange() {
270
249
  // 缓存变更
271
250
  val deviceListJson = JSONArray()
272
- deviceList.forEach{ item ->
251
+ latestDeviceList.forEach{ item ->
273
252
  deviceListJson.put(item.toJsonObject())
274
253
  }
275
254
  sendEvent(EventName.DeviceListListener.name, GizRNCallbackManager.jsonArray2WriteableArray(deviceListJson))
276
255
  }
277
256
 
257
+ override fun invalidate() {
258
+ deviceSubscriptions.cancelAll()
259
+ deviceListEventDispatcher.cancel()
260
+ moduleScope.cancel()
261
+ mReactContext = null
262
+ super.invalidate()
263
+ }
264
+
278
265
  fun sendEvent(name:String, data: WritableArray?) {
279
- mReactContext!!
266
+ val reactContext = mReactContext ?: return
267
+ reactContext
280
268
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
281
269
  .emit(name, data)
282
270
  }
283
271
  fun sendEvent(name:String, data: WritableMap?) {
284
- mReactContext!!
272
+ val reactContext = mReactContext ?: return
273
+ reactContext
285
274
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
286
275
  .emit(name, data)
287
276
  }
@@ -326,6 +315,7 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
326
315
  // 查询 绑定设备 返回设备列表
327
316
  GizSDKManager.queryBoundDevices()
328
317
  deviceListState.take(1).collect {
318
+ latestDeviceList = it
329
319
  val jsonData = JSONArray()
330
320
  it.forEach{device ->
331
321
  jsonData.put(device.toJsonObject())
@@ -337,7 +327,7 @@ class RNGizSDKManagerModule(reactContext: ReactApplicationContext) : ReactContex
337
327
  writableMap.putInt("error", 0)
338
328
  writableMap.putString("message", "")
339
329
  result.invoke(null, writableMap)
340
- emitDeviceListChange()
330
+ requestDeviceListChange()
341
331
  }
342
332
  }
343
333
  }
@@ -0,0 +1,110 @@
1
+ package com.gizwits.reactnativegizwitssdkv5
2
+
3
+ import kotlinx.coroutines.Job
4
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
5
+ import kotlinx.coroutines.test.advanceTimeBy
6
+ import kotlinx.coroutines.test.runCurrent
7
+ import kotlinx.coroutines.test.runTest
8
+ import org.junit.Assert.assertEquals
9
+ import org.junit.Assert.assertFalse
10
+ import org.junit.Assert.assertTrue
11
+ import org.junit.Test
12
+
13
+ @OptIn(ExperimentalCoroutinesApi::class)
14
+ class DeviceListEventCoordinatorTest {
15
+ @Test
16
+ fun requestBeforeWorkerStartsIsDelivered() = runTest {
17
+ var dispatchCount = 0
18
+ val dispatcher = ConflatedEventDispatcher(this, 500) { dispatchCount++ }
19
+
20
+ assertTrue(dispatcher.request())
21
+ advanceTimeBy(500)
22
+ runCurrent()
23
+
24
+ assertEquals(1, dispatchCount)
25
+ dispatcher.cancel()
26
+ }
27
+
28
+ @Test
29
+ fun burstRequestsDeliverOnlyTheLatestSnapshot() = runTest {
30
+ var latestSnapshot = 0
31
+ val deliveredSnapshots = mutableListOf<Int>()
32
+ val dispatcher = ConflatedEventDispatcher(this, 500) {
33
+ deliveredSnapshots += latestSnapshot
34
+ }
35
+
36
+ dispatcher.request()
37
+ runCurrent()
38
+ repeat(5) { value ->
39
+ latestSnapshot = value + 1
40
+ advanceTimeBy(75)
41
+ dispatcher.request()
42
+ }
43
+ advanceTimeBy(125)
44
+ runCurrent()
45
+
46
+ assertEquals(listOf(5), deliveredSnapshots)
47
+ dispatcher.cancel()
48
+ }
49
+
50
+ @Test
51
+ fun continuousRequestsStillDispatchAtEveryWindow() = runTest {
52
+ var dispatchCount = 0
53
+ val dispatcher = ConflatedEventDispatcher(this, 500) { dispatchCount++ }
54
+
55
+ dispatcher.request()
56
+ runCurrent()
57
+ repeat(15) {
58
+ advanceTimeBy(100)
59
+ dispatcher.request()
60
+ runCurrent()
61
+ }
62
+
63
+ assertTrue(dispatchCount >= 2)
64
+ dispatcher.cancel()
65
+ }
66
+
67
+ @Test
68
+ fun registryCancelsRemovedDevicesWithoutDuplicatingRetainedSubscriptions() {
69
+ val createdJobs = mutableMapOf<String, MutableList<Job>>()
70
+ val registry = KeyedSubscriptionRegistry<String, String>(
71
+ keySelector = { it },
72
+ subscribe = { key -> Job().also { createdJobs.getOrPut(key) { mutableListOf() }.add(it) } },
73
+ )
74
+
75
+ registry.update(listOf("a", "b"))
76
+ val firstAJob = createdJobs.getValue("a").single()
77
+ val firstBJob = createdJobs.getValue("b").single()
78
+ registry.update(listOf("b"))
79
+ registry.update(listOf("a", "b"))
80
+
81
+ assertFalse(firstAJob.isActive)
82
+ assertTrue(firstBJob.isActive)
83
+ assertEquals(2, createdJobs.getValue("a").size)
84
+ assertEquals(1, createdJobs.getValue("b").size)
85
+ assertEquals(setOf("a", "b"), registry.keys())
86
+
87
+ registry.cancelAll()
88
+ assertTrue(createdJobs.values.flatten().none { it.isActive })
89
+ assertTrue(registry.keys().isEmpty())
90
+ }
91
+
92
+ @Test
93
+ fun registryReplacesSubscriptionWhenValueInstanceChangesForTheSameKey() {
94
+ data class Device(val id: String)
95
+
96
+ val jobs = mutableListOf<Job>()
97
+ val registry = KeyedSubscriptionRegistry<String, Device>(
98
+ keySelector = Device::id,
99
+ subscribe = { Job().also(jobs::add) },
100
+ )
101
+
102
+ registry.update(listOf(Device("a")))
103
+ registry.update(listOf(Device("a")))
104
+
105
+ assertEquals(2, jobs.size)
106
+ assertFalse(jobs.first().isActive)
107
+ assertTrue(jobs.last().isActive)
108
+ registry.cancelAll()
109
+ }
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gizwits-sdk-v5",
3
- "version": "1.7.7-beta",
3
+ "version": "1.7.9-beta",
4
4
  "description": "Gizwits",
5
5
  "homepage": "https://github.com/demchenkoalex/react-native-gizwits-sdk-v5#readme",
6
6
  "main": "lib/index.js",
@@ -19,6 +19,6 @@ Pod::Spec.new do |s|
19
19
 
20
20
  s.dependency 'React'
21
21
  s.dependency 'CryptoSwift', '~> 1.8.0'
22
- s.dependency 'GizwitsiOSSDK', '~> 2.1.6'
22
+ s.dependency 'GizwitsiOSSDK', '~> 2.1.7'
23
23
 
24
24
  end