@mentra/bluetooth-sdk 3.2.0-dev.200 → 3.2.0-dev.206
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.
- package/README.md +46 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +63 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +5 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +298 -181
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +326 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbe.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamSessionState.kt +32 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbeTest.kt +24 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamSessionStateTest.kt +43 -0
- package/build/BluetoothSdk.types.d.ts +68 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -13
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +6 -3
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +372 -220
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/sgcs/MentraLive.swift +216 -70
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +28 -0
- package/ios/Source/streaming/StreamControllerProbe.swift +18 -0
- package/ios/Source/streaming/StreamModels.swift +12 -0
- package/ios/Source/streaming/StreamSessionState.swift +29 -0
- package/ios/Tests/StreamControllerProbeTests.swift +27 -0
- package/ios/Tests/StreamSessionStateTests.swift +38 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +82 -2
- package/src/_private/BluetoothSdkModule.ts +4 -4
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
internal fun wifiSsidIsValid(ssid: String): Boolean = ssid.trim().isNotEmpty()
|
|
4
|
+
|
|
5
|
+
/** Validate raw fields before JSON convenience getters can coerce numbers or erase presence. */
|
|
6
|
+
internal fun wifiResponseEnvelopeIsValid(values: Map<String, Any>, allowLegacy: Boolean): Boolean {
|
|
7
|
+
if (values.containsKey("connected") && values["connected"] !is Boolean) return false
|
|
8
|
+
if (values.containsKey("dispatched") && values["dispatched"] !is Boolean) return false
|
|
9
|
+
val hasTuple = listOf("protocol_version", "requestId", "sid").any(values::containsKey)
|
|
10
|
+
if (!hasTuple) return allowLegacy && !values.containsKey("outcome")
|
|
11
|
+
return (values["protocol_version"] as? Number)?.toDouble() == 1.0 &&
|
|
12
|
+
(values["requestId"] as? String)?.isNotBlank() == true &&
|
|
13
|
+
(values["sid"] as? String)?.isNotBlank() == true &&
|
|
14
|
+
!values.containsKey("dispatched")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
internal const val WIFI_CAPABILITY_NEGOTIATION_TIMEOUT_CODE = "capability_negotiation_timeout"
|
|
18
|
+
|
|
19
|
+
internal sealed class WifiProtocolCapability {
|
|
20
|
+
data object Unknown : WifiProtocolCapability()
|
|
21
|
+
|
|
22
|
+
data class Supported(val version: Int) : WifiProtocolCapability()
|
|
23
|
+
|
|
24
|
+
data object Unsupported : WifiProtocolCapability()
|
|
25
|
+
|
|
26
|
+
data object Legacy : WifiProtocolCapability()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
internal enum class WifiRequestMode {
|
|
30
|
+
DISCOVERING,
|
|
31
|
+
MODERN,
|
|
32
|
+
LEGACY,
|
|
33
|
+
UNSUPPORTED,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
internal data class WifiSessionRequestSnapshot(
|
|
37
|
+
val mode: WifiRequestMode,
|
|
38
|
+
val sessionId: String,
|
|
39
|
+
val epoch: Long,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
internal class WifiSessionCapabilities {
|
|
43
|
+
var sessionId: String = ""
|
|
44
|
+
private set
|
|
45
|
+
var epoch: Long = 0
|
|
46
|
+
private set
|
|
47
|
+
var forgetResult: WifiProtocolCapability = WifiProtocolCapability.Unknown
|
|
48
|
+
private set
|
|
49
|
+
var savedNetworks: WifiProtocolCapability = WifiProtocolCapability.Unknown
|
|
50
|
+
private set
|
|
51
|
+
|
|
52
|
+
fun reset(sessionId: String = "") {
|
|
53
|
+
epoch += 1
|
|
54
|
+
this.sessionId = sessionId
|
|
55
|
+
forgetResult = WifiProtocolCapability.Unknown
|
|
56
|
+
savedNetworks = WifiProtocolCapability.Unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun applyVersionInfo1(values: Map<String, Any>) {
|
|
60
|
+
(values["sid"] as? String)?.takeIf { it.isNotEmpty() }?.let { sessionId = it }
|
|
61
|
+
forgetResult = capability(values["wifiForgetResultVersion"])
|
|
62
|
+
savedNetworks = capability(values["savedWifiNetworksVersion"])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fun forgetMode(): WifiRequestMode = requestMode(forgetResult)
|
|
66
|
+
|
|
67
|
+
fun savedNetworksMode(): WifiRequestMode = requestMode(savedNetworks)
|
|
68
|
+
|
|
69
|
+
fun savedNetworksRequestSnapshot(): WifiSessionRequestSnapshot =
|
|
70
|
+
WifiSessionRequestSnapshot(savedNetworksMode(), sessionId, epoch)
|
|
71
|
+
|
|
72
|
+
private fun capability(raw: Any?): WifiProtocolCapability {
|
|
73
|
+
if (raw == null) return WifiProtocolCapability.Legacy
|
|
74
|
+
return if (raw is Number && raw.toDouble() == 1.0 && sessionId.isNotEmpty()) {
|
|
75
|
+
WifiProtocolCapability.Supported(1)
|
|
76
|
+
} else WifiProtocolCapability.Unsupported
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun requestMode(capability: WifiProtocolCapability): WifiRequestMode =
|
|
80
|
+
when (capability) {
|
|
81
|
+
WifiProtocolCapability.Unknown -> WifiRequestMode.DISCOVERING
|
|
82
|
+
is WifiProtocolCapability.Supported -> WifiRequestMode.MODERN
|
|
83
|
+
WifiProtocolCapability.Legacy -> WifiRequestMode.LEGACY
|
|
84
|
+
WifiProtocolCapability.Unsupported -> WifiRequestMode.UNSUPPORTED
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
enum class WifiForgetOutcome(val wireValue: String) {
|
|
89
|
+
CONFIRMED("confirmed"),
|
|
90
|
+
DISPATCHED("dispatched"),
|
|
91
|
+
NOT_FOUND("not_found"),
|
|
92
|
+
UNSUPPORTED("unsupported"),
|
|
93
|
+
FAILED("failed"),
|
|
94
|
+
LEGACY_UNVERIFIED("legacy_unverified");
|
|
95
|
+
|
|
96
|
+
companion object {
|
|
97
|
+
internal fun fromWire(value: String): WifiForgetOutcome? =
|
|
98
|
+
entries.find { it.wireValue == value }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
internal fun normalizeWifiForgetResultEvent(
|
|
103
|
+
requestId: String,
|
|
104
|
+
sid: String,
|
|
105
|
+
ssid: String,
|
|
106
|
+
protocolVersion: Int,
|
|
107
|
+
outcome: String,
|
|
108
|
+
legacyDispatched: Boolean?,
|
|
109
|
+
connected: Boolean?,
|
|
110
|
+
currentSsid: String,
|
|
111
|
+
localIp: String,
|
|
112
|
+
error: String?,
|
|
113
|
+
): Map<String, Any>? {
|
|
114
|
+
if (!wifiSsidIsValid(ssid)) return null
|
|
115
|
+
val modernOutcome = WifiForgetOutcome.fromWire(outcome)
|
|
116
|
+
if (requestId.isNotEmpty() && sid.isNotEmpty() && protocolVersion == 1 &&
|
|
117
|
+
modernOutcome != null && modernOutcome != WifiForgetOutcome.LEGACY_UNVERIFIED
|
|
118
|
+
) {
|
|
119
|
+
return buildMap {
|
|
120
|
+
put("mode", "modern")
|
|
121
|
+
put("requestId", requestId)
|
|
122
|
+
put("sid", sid)
|
|
123
|
+
put("ssid", ssid)
|
|
124
|
+
put("protocolVersion", protocolVersion)
|
|
125
|
+
put("outcome", modernOutcome.wireValue)
|
|
126
|
+
connected?.let { put("connected", it) }
|
|
127
|
+
if (currentSsid.isNotEmpty()) put("currentSsid", currentSsid)
|
|
128
|
+
if (localIp.isNotEmpty()) put("localIp", localIp)
|
|
129
|
+
error?.let { put("error", it) }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (requestId.isEmpty() && sid.isEmpty() && protocolVersion == 0 && outcome.isEmpty() && legacyDispatched != null) {
|
|
133
|
+
return buildMap {
|
|
134
|
+
put("mode", "legacy")
|
|
135
|
+
put("ssid", ssid)
|
|
136
|
+
put("dispatched", legacyDispatched)
|
|
137
|
+
connected?.let { put("connected", it) }
|
|
138
|
+
if (currentSsid.isNotEmpty()) put("currentSsid", currentSsid)
|
|
139
|
+
if (localIp.isNotEmpty()) put("localIp", localIp)
|
|
140
|
+
error?.let { put("error", it) }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
data class WifiForgetResult(
|
|
147
|
+
val ssid: String,
|
|
148
|
+
val outcome: WifiForgetOutcome,
|
|
149
|
+
val connected: Boolean?,
|
|
150
|
+
val currentSsid: String?,
|
|
151
|
+
val localIp: String?,
|
|
152
|
+
val error: String? = null,
|
|
153
|
+
) {
|
|
154
|
+
internal fun toMap(): Map<String, Any> =
|
|
155
|
+
buildMap {
|
|
156
|
+
put("ssid", ssid)
|
|
157
|
+
put("outcome", outcome.wireValue)
|
|
158
|
+
connected?.let { put("connected", it) }
|
|
159
|
+
currentSsid?.let { put("currentSsid", it) }
|
|
160
|
+
localIp?.let { put("localIp", it) }
|
|
161
|
+
error?.let { put("error", it) }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
enum class SavedWifiNetworksOutcome(val wireValue: String) {
|
|
166
|
+
CONFIRMED("confirmed"),
|
|
167
|
+
UNSUPPORTED("unsupported"),
|
|
168
|
+
FAILED("failed");
|
|
169
|
+
|
|
170
|
+
companion object {
|
|
171
|
+
internal fun fromWire(value: String): SavedWifiNetworksOutcome? =
|
|
172
|
+
entries.find { it.wireValue == value }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
data class SavedWifiNetworksResult(
|
|
177
|
+
val outcome: SavedWifiNetworksOutcome,
|
|
178
|
+
val networks: List<String>,
|
|
179
|
+
val error: String? = null,
|
|
180
|
+
) {
|
|
181
|
+
internal fun toMap(): Map<String, Any> =
|
|
182
|
+
buildMap {
|
|
183
|
+
put("outcome", outcome.wireValue)
|
|
184
|
+
put("networks", networks)
|
|
185
|
+
error?.let { put("error", it) }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
internal fun parseWifiForgetResult(
|
|
190
|
+
expectedRequestId: String,
|
|
191
|
+
expectedSid: String,
|
|
192
|
+
expectedSsid: String,
|
|
193
|
+
capabilityVersion: Int,
|
|
194
|
+
data: Map<String, Any>,
|
|
195
|
+
): WifiForgetResult? {
|
|
196
|
+
if (capabilityVersion != 1 || expectedRequestId.isEmpty() || expectedSid.isEmpty()) return null
|
|
197
|
+
if (data["requestId"] as? String != expectedRequestId) return null
|
|
198
|
+
if (data["sid"] as? String != expectedSid) return null
|
|
199
|
+
if (data["ssid"] as? String != expectedSsid) return null
|
|
200
|
+
if ((data["protocolVersion"] as? Number)?.toDouble() != 1.0) return null
|
|
201
|
+
val outcome = WifiForgetOutcome.fromWire(data["outcome"] as? String ?: return null) ?: return null
|
|
202
|
+
if (outcome == WifiForgetOutcome.LEGACY_UNVERIFIED) return null
|
|
203
|
+
return WifiForgetResult(
|
|
204
|
+
ssid = expectedSsid,
|
|
205
|
+
outcome = outcome,
|
|
206
|
+
connected = data["connected"] as? Boolean,
|
|
207
|
+
currentSsid = data["currentSsid"] as? String,
|
|
208
|
+
localIp = data["localIp"] as? String,
|
|
209
|
+
error = (data["error"] as? String)?.takeIf { it.isNotEmpty() },
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
internal fun parseSavedWifiNetworks(
|
|
214
|
+
expectedRequestId: String,
|
|
215
|
+
expectedSid: String,
|
|
216
|
+
capabilityVersion: Int,
|
|
217
|
+
data: Map<String, Any>,
|
|
218
|
+
): SavedWifiNetworksResult? {
|
|
219
|
+
if (capabilityVersion != 1 || expectedRequestId.isEmpty() || expectedSid.isEmpty()) return null
|
|
220
|
+
if (data["requestId"] as? String != expectedRequestId) return null
|
|
221
|
+
if (data["sid"] as? String != expectedSid) return null
|
|
222
|
+
if ((data["protocolVersion"] as? Number)?.toDouble() != 1.0) return null
|
|
223
|
+
val outcome =
|
|
224
|
+
SavedWifiNetworksOutcome.fromWire(data["outcome"] as? String ?: return null) ?: return null
|
|
225
|
+
val rawNetworks = data["networks"] as? List<*> ?: return null
|
|
226
|
+
if (rawNetworks.any { it !is String }) return null
|
|
227
|
+
if (outcome != SavedWifiNetworksOutcome.CONFIRMED && rawNetworks.isNotEmpty()) return null
|
|
228
|
+
val networks = rawNetworks.map { it as String }.filter { it.trim().isNotEmpty() }.distinct()
|
|
229
|
+
return SavedWifiNetworksResult(
|
|
230
|
+
outcome = outcome,
|
|
231
|
+
networks = networks,
|
|
232
|
+
error = (data["error"] as? String)?.takeIf { it.isNotEmpty() },
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
internal fun legacyWifiForgetResult(
|
|
237
|
+
ssid: String,
|
|
238
|
+
): WifiForgetResult {
|
|
239
|
+
return WifiForgetResult(
|
|
240
|
+
ssid = ssid,
|
|
241
|
+
outcome = WifiForgetOutcome.LEGACY_UNVERIFIED,
|
|
242
|
+
connected = null,
|
|
243
|
+
currentSsid = null,
|
|
244
|
+
localIp = null,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
internal enum class ClassicAudioProfile {
|
|
4
|
+
A2DP,
|
|
5
|
+
HEADSET,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** A confirmed connection is sufficient; declaring disconnection requires both profile reads. */
|
|
9
|
+
internal fun readyClassicAudioSnapshot(states: Map<ClassicAudioProfile, Boolean>): Set<ClassicAudioProfile>? {
|
|
10
|
+
val connected = states.filterValues { it }.keys
|
|
11
|
+
return if (connected.isNotEmpty() || states.size == ClassicAudioProfile.entries.size) connected else null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tracks the Android Classic audio profiles for one dual-mode glasses device.
|
|
16
|
+
*
|
|
17
|
+
* Android reports A2DP and HFP independently. Keeping both states prevents an A2DP disconnect from
|
|
18
|
+
* publishing a false negative while HFP is still connected. Snapshot tickets reject obsolete
|
|
19
|
+
* queries, including a previous session with the same MAC address. Broadcast payloads are not truth.
|
|
20
|
+
*/
|
|
21
|
+
internal class ClassicAudioConnectionTracker(
|
|
22
|
+
private val onConnectedChanged: (Boolean) -> Unit,
|
|
23
|
+
) {
|
|
24
|
+
private var targetAddress: String? = null
|
|
25
|
+
private var revision = 0L
|
|
26
|
+
private val connectedProfiles = mutableSetOf<ClassicAudioProfile>()
|
|
27
|
+
|
|
28
|
+
@get:Synchronized
|
|
29
|
+
val connected: Boolean
|
|
30
|
+
get() = connectedProfiles.isNotEmpty()
|
|
31
|
+
|
|
32
|
+
@Synchronized
|
|
33
|
+
fun setTarget(address: String) {
|
|
34
|
+
val normalizedAddress = address.normalizedBluetoothAddress()
|
|
35
|
+
revision++
|
|
36
|
+
if (targetAddress == normalizedAddress) return
|
|
37
|
+
|
|
38
|
+
targetAddress = normalizedAddress
|
|
39
|
+
clearConnectedProfiles()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Synchronized
|
|
43
|
+
fun beginSnapshot(address: String): Long? {
|
|
44
|
+
if (targetAddress != address.normalizedBluetoothAddress()) return null
|
|
45
|
+
return ++revision
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Both profile queries must belong to the newest request for this session. */
|
|
49
|
+
@Synchronized
|
|
50
|
+
fun applySnapshot(
|
|
51
|
+
ticket: Long,
|
|
52
|
+
address: String,
|
|
53
|
+
profiles: Set<ClassicAudioProfile>,
|
|
54
|
+
onAccepted: () -> Unit = {},
|
|
55
|
+
): Boolean {
|
|
56
|
+
if (ticket != revision || targetAddress != address.normalizedBluetoothAddress()) return false
|
|
57
|
+
val wasConnected = this.connected
|
|
58
|
+
connectedProfiles.clear()
|
|
59
|
+
connectedProfiles.addAll(profiles)
|
|
60
|
+
publishIfChanged(wasConnected)
|
|
61
|
+
// Audio routing notifications must not race a target invalidation after validation.
|
|
62
|
+
onAccepted()
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Synchronized
|
|
67
|
+
fun clear(address: String): Boolean {
|
|
68
|
+
if (targetAddress != address.normalizedBluetoothAddress()) return false
|
|
69
|
+
revision++
|
|
70
|
+
clearConnectedProfiles()
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Synchronized
|
|
75
|
+
fun invalidate(address: String): Boolean {
|
|
76
|
+
if (targetAddress != address.normalizedBluetoothAddress()) return false
|
|
77
|
+
|
|
78
|
+
targetAddress = null
|
|
79
|
+
revision++
|
|
80
|
+
clearConnectedProfiles()
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Synchronized
|
|
85
|
+
fun reset() {
|
|
86
|
+
targetAddress = null
|
|
87
|
+
revision++
|
|
88
|
+
clearConnectedProfiles()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private fun clearConnectedProfiles() {
|
|
92
|
+
val wasConnected = connected
|
|
93
|
+
connectedProfiles.clear()
|
|
94
|
+
publishIfChanged(wasConnected)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun publishIfChanged(wasConnected: Boolean) {
|
|
98
|
+
if (wasConnected != connected) {
|
|
99
|
+
onConnectedChanged(connected)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun String.normalizedBluetoothAddress(): String = trim().uppercase()
|