@mentra/bluetooth-sdk 3.3.0-dev.382 → 3.3.0-dev.391
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/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +7 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +8 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +15 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +7 -3
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/sgcs/G1.swift +21 -6
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +5 -2
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +53 -0
- package/package.json +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.bluetooth.BluetoothDevice
|
|
5
|
+
import android.bluetooth.BluetoothGatt
|
|
6
|
+
import android.bluetooth.BluetoothProfile
|
|
7
|
+
import android.bluetooth.le.ScanResult
|
|
8
|
+
import android.bluetooth.le.ScanSettings
|
|
9
|
+
import android.os.Looper
|
|
10
|
+
import androidx.test.core.app.ApplicationProvider
|
|
11
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
12
|
+
import com.mentra.bluetoothsdk.DeviceManager
|
|
13
|
+
import com.mentra.bluetoothsdk.DeviceStore
|
|
14
|
+
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
15
|
+
import org.junit.Assert.assertEquals
|
|
16
|
+
import org.junit.Assert.assertTrue
|
|
17
|
+
import org.junit.Before
|
|
18
|
+
import org.junit.Test
|
|
19
|
+
import org.junit.runner.RunWith
|
|
20
|
+
import org.robolectric.RobolectricTestRunner
|
|
21
|
+
import org.robolectric.Shadows.shadowOf
|
|
22
|
+
import org.robolectric.annotation.Config
|
|
23
|
+
import org.robolectric.annotation.Implementation
|
|
24
|
+
import org.robolectric.annotation.Implements
|
|
25
|
+
import org.robolectric.annotation.LooperMode
|
|
26
|
+
import org.robolectric.shadows.ShadowBluetoothGatt
|
|
27
|
+
|
|
28
|
+
@RunWith(RobolectricTestRunner::class)
|
|
29
|
+
@Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class, MentraNexPairingTest.ShadowNexGatt::class], instrumentedPackages = ["com.mentra.lc3Lib"])
|
|
30
|
+
@LooperMode(LooperMode.Mode.PAUSED)
|
|
31
|
+
class MentraNexPairingTest {
|
|
32
|
+
// Robolectric implements connection callbacks but not the PHY binder request.
|
|
33
|
+
@Implements(BluetoothGatt::class)
|
|
34
|
+
class ShadowNexGatt : ShadowBluetoothGatt() {
|
|
35
|
+
@Implementation fun setPreferredPhy(txPhy: Int, rxPhy: Int, phyOptions: Int) {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Before fun setup() {
|
|
39
|
+
Bridge.initialize(ApplicationProvider.getApplicationContext())
|
|
40
|
+
shadowOf(BluetoothAdapter.getDefaultAdapter()).setState(BluetoothAdapter.STATE_ON)
|
|
41
|
+
DeviceStore.set("bluetooth", "pending_device_name", "Nex1-2")
|
|
42
|
+
DeviceStore.set("bluetooth", "pending_device_address", "")
|
|
43
|
+
DeviceStore.set("bluetooth", "device_address", "")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test fun `fresh pairing selects only the requested Nex`() {
|
|
47
|
+
assertScansSelectedTarget("", "")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test fun `simulated pairing selects only the requested Nex`() {
|
|
51
|
+
assertScansSelectedTarget("Simulated Glasses", "")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test fun `a different saved Nex name and address cannot redirect selection`() {
|
|
55
|
+
assertScansSelectedTarget("Nex1-1", "00:00:00:00:00:01")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test fun `pending address targets the new Nex instead of the saved pair`() {
|
|
59
|
+
assertDirectTarget("Nex1-1", "00:00:00:00:00:01", "00:00:00:00:00:02")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test fun `saved Nex reconnect retains its address`() {
|
|
63
|
+
assertDirectTarget("Nex1-2", "00:00:00:00:00:02", "")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test fun `name-only pairing readiness cannot associate the old MAC with the new Nex`() {
|
|
67
|
+
DeviceStore.set("bluetooth", "default_wearable", DeviceTypes.NEX)
|
|
68
|
+
DeviceStore.set("bluetooth", "pending_wearable", DeviceTypes.NEX)
|
|
69
|
+
DeviceStore.set("bluetooth", "device_name", "Nex1-1")
|
|
70
|
+
DeviceStore.set("bluetooth", "device_address", "00:00:00:00:00:01")
|
|
71
|
+
DeviceStore.set("bluetooth", "shouldSendBootingMessage", false)
|
|
72
|
+
DeviceStore.set("glasses", "fullyBooted", false)
|
|
73
|
+
val old = device("00:00:00:00:00:01", "Nex1-1")
|
|
74
|
+
val selected = device("00:00:00:00:00:02", "Nex1-2")
|
|
75
|
+
val singleton = DeviceManager::class.java.getDeclaredField("_instance").apply { isAccessible = true }
|
|
76
|
+
val previous = singleton.get(null)
|
|
77
|
+
val manager = DeviceManager(initializeHardware = false)
|
|
78
|
+
singleton.set(null, manager)
|
|
79
|
+
try {
|
|
80
|
+
var nex = MentraNex()
|
|
81
|
+
manager.sgc = nex
|
|
82
|
+
nex.connectById("Nex1-2")
|
|
83
|
+
val scanner = shadowOf(BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner)
|
|
84
|
+
scanner.scanCallbacks.single().onScanResult(
|
|
85
|
+
ScanSettings.CALLBACK_TYPE_ALL_MATCHES, ScanResult(selected, null, -50, 0),
|
|
86
|
+
)
|
|
87
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
88
|
+
// Drive the actual ready callback and DeviceManager's identity promotion.
|
|
89
|
+
shadowOf(selected).simulateGattConnectionChange(BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED)
|
|
90
|
+
assertEquals("Nex1-2", DeviceStore.get("bluetooth", "device_name"))
|
|
91
|
+
assertEquals("", DeviceStore.get("bluetooth", "device_address"))
|
|
92
|
+
assertEquals("", DeviceStore.get("bluetooth", "pending_device_name"))
|
|
93
|
+
manager.disconnect()
|
|
94
|
+
|
|
95
|
+
nex = MentraNex()
|
|
96
|
+
manager.sgc = nex
|
|
97
|
+
nex.connectById(DeviceStore.get("bluetooth", "device_name") as String)
|
|
98
|
+
assertTrue(shadowOf(old).bluetoothGatts.isEmpty())
|
|
99
|
+
scanner.scanCallbacks.single().onScanResult(
|
|
100
|
+
ScanSettings.CALLBACK_TYPE_ALL_MATCHES, ScanResult(selected, null, -50, 0),
|
|
101
|
+
)
|
|
102
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
103
|
+
assertEquals(2, shadowOf(selected).bluetoothGatts.size)
|
|
104
|
+
} finally {
|
|
105
|
+
manager.disconnect()
|
|
106
|
+
manager.cleanup()
|
|
107
|
+
singleton.set(null, previous)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun assertScansSelectedTarget(savedName: String, savedAddress: String) {
|
|
112
|
+
DeviceStore.set("bluetooth", "device_name", savedName)
|
|
113
|
+
DeviceStore.set("bluetooth", "device_address", savedAddress)
|
|
114
|
+
val old = device("00:00:00:00:00:01", "Nex1-1")
|
|
115
|
+
val prefixCollision = device("00:00:00:00:00:03", "Nex1-23")
|
|
116
|
+
val selected = device("00:00:00:00:00:02", "Nex1-2")
|
|
117
|
+
val nex = MentraNex()
|
|
118
|
+
try {
|
|
119
|
+
nex.connectById("Nex1-2")
|
|
120
|
+
val scanner = shadowOf(BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner)
|
|
121
|
+
val callback = scanner.scanCallbacks.single()
|
|
122
|
+
for (found in listOf(old, prefixCollision, selected)) {
|
|
123
|
+
callback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, ScanResult(found, null, -50, 0))
|
|
124
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
125
|
+
}
|
|
126
|
+
assertTrue(shadowOf(old).bluetoothGatts.isEmpty())
|
|
127
|
+
assertTrue(shadowOf(prefixCollision).bluetoothGatts.isEmpty())
|
|
128
|
+
assertEquals(1, shadowOf(selected).bluetoothGatts.size)
|
|
129
|
+
assertEquals(savedName, DeviceStore.get("bluetooth", "device_name"))
|
|
130
|
+
} finally { nex.disconnect() }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun assertDirectTarget(savedName: String, savedAddress: String, pendingAddress: String) {
|
|
134
|
+
DeviceStore.set("bluetooth", "device_name", savedName)
|
|
135
|
+
DeviceStore.set("bluetooth", "device_address", savedAddress)
|
|
136
|
+
DeviceStore.set("bluetooth", "pending_device_address", pendingAddress)
|
|
137
|
+
val old = device("00:00:00:00:00:01", "Nex1-1")
|
|
138
|
+
val selected = device("00:00:00:00:00:02", "Nex1-2")
|
|
139
|
+
val nex = MentraNex()
|
|
140
|
+
try {
|
|
141
|
+
nex.connectById("Nex1-2")
|
|
142
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
143
|
+
assertTrue(shadowOf(old).bluetoothGatts.isEmpty())
|
|
144
|
+
assertEquals(1, shadowOf(selected).bluetoothGatts.size)
|
|
145
|
+
assertEquals(savedName, DeviceStore.get("bluetooth", "device_name"))
|
|
146
|
+
} finally { nex.disconnect() }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun device(address: String, name: String): BluetoothDevice =
|
|
150
|
+
BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address).also { shadowOf(it).setName(name) }
|
|
151
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class SelectedDeviceAddressTest {
|
|
8
|
+
@Test fun `selected pending address wins over saved address`() {
|
|
9
|
+
assertEquals("new", SelectedDeviceAddress.resolve("LIVE_2", "LIVE_2", "new", "LIVE_1", "old"))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Test fun `a name-only selection cannot use another device's saved address`() {
|
|
13
|
+
assertNull(SelectedDeviceAddress.resolve("LIVE_2", "LIVE_2", "", "LIVE_1", "old"))
|
|
14
|
+
assertNull(SelectedDeviceAddress.resolve("Nex1-2", "Nex1-2", null, "Nex1-1", "old"))
|
|
15
|
+
assertNull(SelectedDeviceAddress.resolve("Nex1-2", null, null, "Simulated Glasses", ""))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test fun `saved reconnect keeps its address even with an unrelated pending selection`() {
|
|
19
|
+
assertEquals("old", SelectedDeviceAddress.resolve("LIVE_1", "LIVE_2", "new", "LIVE_1", "old"))
|
|
20
|
+
assertEquals("old", SelectedDeviceAddress.resolve("Nex1-1", null, null, "Nex1-1", "old"))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test fun `empty or mismatched identities never supply an address`() {
|
|
24
|
+
assertNull(SelectedDeviceAddress.resolve("", "", "new", "", "old"))
|
|
25
|
+
assertNull(SelectedDeviceAddress.resolve("LIVE_2", "LIVE_1", "new", "LIVE_1", "old"))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "3.3.0",
|
|
4
|
-
"releaseIdentity": "3.3.0-dev.
|
|
5
|
-
"releaseSetId": "mentra-3.3.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
7
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
8
|
-
"otaManifestSha256": "
|
|
4
|
+
"releaseIdentity": "3.3.0-dev.391",
|
|
5
|
+
"releaseSetId": "mentra-3.3.0-dev.391",
|
|
6
|
+
"sourceCommit": "2763761a9cff7660e63412e9d006b4e20033f90b",
|
|
7
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.391.json",
|
|
8
|
+
"otaManifestSha256": "c079797bc98050c666d513a4960fd41a6fb5dba5ea809f876954633ae08621fa"
|
|
9
9
|
});
|
|
10
10
|
//# sourceMappingURL=releaseMetadata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,iIAAiI;IACnJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.3.0\",\n \"releaseIdentity\": \"3.3.0-dev.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,iIAAiI;IACnJ,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.3.0\",\n \"releaseIdentity\": \"3.3.0-dev.391\",\n \"releaseSetId\": \"mentra-3.3.0-dev.391\",\n \"sourceCommit\": \"2763761a9cff7660e63412e9d006b4e20033f90b\",\n \"otaManifestUrl\": \"https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.391.json\",\n \"otaManifestSha256\": \"c079797bc98050c666d513a4960fd41a6fb5dba5ea809f876954633ae08621fa\"\n})\n"]}
|
|
@@ -16,7 +16,7 @@ enum BluetoothSdkDefaults {
|
|
|
16
16
|
/// silently disable it on every pair the app touches.
|
|
17
17
|
static let autoPowerOffEnabled = false
|
|
18
18
|
private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
|
|
19
|
-
private static let swiftPackageSdkVersion = "3.3.0-dev.
|
|
19
|
+
private static let swiftPackageSdkVersion = "3.3.0-dev.391"
|
|
20
20
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
21
21
|
|
|
22
22
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -998,12 +998,16 @@ struct ViewState {
|
|
|
998
998
|
Bridge.log("MAN: handleDeviceReady(): \(sgc.type)")
|
|
999
999
|
resetMicHealth()
|
|
1000
1000
|
|
|
1001
|
+
// A new identity must never inherit the previous glasses' address when
|
|
1002
|
+
// pairing supplied only a name. Same-device reconnects retain their cache.
|
|
1003
|
+
let identityChanged = !pendingDeviceName.isEmpty &&
|
|
1004
|
+
(pendingDeviceName != deviceName || sgc.type != defaultWearable)
|
|
1005
|
+
if identityChanged || !pendingDeviceAddress.isEmpty {
|
|
1006
|
+
deviceAddress = pendingDeviceAddress
|
|
1007
|
+
}
|
|
1001
1008
|
if !pendingDeviceName.isEmpty {
|
|
1002
1009
|
deviceName = pendingDeviceName
|
|
1003
1010
|
}
|
|
1004
|
-
if !pendingDeviceAddress.isEmpty {
|
|
1005
|
-
deviceAddress = pendingDeviceAddress
|
|
1006
|
-
}
|
|
1007
1011
|
clearPendingConnection()
|
|
1008
1012
|
defaultWearable = sgc.type
|
|
1009
1013
|
searching = false
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "3.3.0"
|
|
6
|
-
static let releaseIdentity = "3.3.0-dev.
|
|
7
|
-
static let releaseSetId = "mentra-3.3.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.
|
|
10
|
-
static let otaManifestSha256 = "
|
|
6
|
+
static let releaseIdentity = "3.3.0-dev.391"
|
|
7
|
+
static let releaseSetId = "mentra-3.3.0-dev.391"
|
|
8
|
+
static let sourceCommit = "2763761a9cff7660e63412e9d006b4e20033f90b"
|
|
9
|
+
static let otaManifestUrl = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.3.0/mentra-live-ota-3.3.0-dev.391.json"
|
|
10
|
+
static let otaManifestSha256 = "c079797bc98050c666d513a4960fd41a6fb5dba5ea809f876954633ae08621fa"
|
|
11
11
|
}
|
package/ios/Source/sgcs/G1.swift
CHANGED
|
@@ -468,8 +468,10 @@ class G1: NSObject, SGCManager {
|
|
|
468
468
|
set {
|
|
469
469
|
if let newValue = newValue {
|
|
470
470
|
UserDefaults.standard.set(newValue.uuidString, forKey: "leftGlassUUID")
|
|
471
|
+
UserDefaults.standard.set(DEVICE_SEARCH_ID, forKey: "leftGlassSearchID")
|
|
471
472
|
} else {
|
|
472
473
|
UserDefaults.standard.removeObject(forKey: "leftGlassUUID")
|
|
474
|
+
UserDefaults.standard.removeObject(forKey: "leftGlassSearchID")
|
|
473
475
|
}
|
|
474
476
|
}
|
|
475
477
|
}
|
|
@@ -484,8 +486,10 @@ class G1: NSObject, SGCManager {
|
|
|
484
486
|
set {
|
|
485
487
|
if let newValue = newValue {
|
|
486
488
|
UserDefaults.standard.set(newValue.uuidString, forKey: "rightGlassUUID")
|
|
489
|
+
UserDefaults.standard.set(DEVICE_SEARCH_ID, forKey: "rightGlassSearchID")
|
|
487
490
|
} else {
|
|
488
491
|
UserDefaults.standard.removeObject(forKey: "rightGlassUUID")
|
|
492
|
+
UserDefaults.standard.removeObject(forKey: "rightGlassSearchID")
|
|
489
493
|
}
|
|
490
494
|
}
|
|
491
495
|
}
|
|
@@ -2401,17 +2405,22 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
|
2401
2405
|
}
|
|
2402
2406
|
|
|
2403
2407
|
Bridge.log("G1: 🔵 Attempting to connect by UUID")
|
|
2404
|
-
var
|
|
2408
|
+
var foundSides = 0
|
|
2405
2409
|
|
|
2406
2410
|
if let leftUUID = leftGlassUUID {
|
|
2407
2411
|
Bridge.log("G1: 🔵 Found stored left glass UUID: \(leftUUID.uuidString)")
|
|
2408
2412
|
let leftDevices = centralManager!.retrievePeripherals(withIdentifiers: [leftUUID])
|
|
2409
2413
|
|
|
2410
|
-
if let leftDevice = leftDevices.first
|
|
2414
|
+
if let leftDevice = leftDevices.first,
|
|
2415
|
+
G1ConnectionTarget.matchesCachedPeripheral(
|
|
2416
|
+
name: leftDevice.name, searchID: DEVICE_SEARCH_ID,
|
|
2417
|
+
cachedSearchID: UserDefaults.standard.string(forKey: "leftGlassSearchID")
|
|
2418
|
+
)
|
|
2419
|
+
{
|
|
2411
2420
|
Bridge.log(
|
|
2412
2421
|
"G1: 🔵 Successfully retrieved left glass: \(leftDevice.name ?? "Unknown")"
|
|
2413
2422
|
)
|
|
2414
|
-
|
|
2423
|
+
foundSides += 1
|
|
2415
2424
|
leftPeripheral = leftDevice
|
|
2416
2425
|
leftDevice.delegate = self
|
|
2417
2426
|
centralManager!.connect(
|
|
@@ -2428,11 +2437,16 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
|
2428
2437
|
Bridge.log("G1: 🔵 Found stored right glass UUID: \(rightUUID.uuidString)")
|
|
2429
2438
|
let rightDevices = centralManager!.retrievePeripherals(withIdentifiers: [rightUUID])
|
|
2430
2439
|
|
|
2431
|
-
if let rightDevice = rightDevices.first
|
|
2440
|
+
if let rightDevice = rightDevices.first,
|
|
2441
|
+
G1ConnectionTarget.matchesCachedPeripheral(
|
|
2442
|
+
name: rightDevice.name, searchID: DEVICE_SEARCH_ID,
|
|
2443
|
+
cachedSearchID: UserDefaults.standard.string(forKey: "rightGlassSearchID")
|
|
2444
|
+
)
|
|
2445
|
+
{
|
|
2432
2446
|
Bridge.log(
|
|
2433
2447
|
"G1: 🔵 Successfully retrieved right glass: \(rightDevice.name ?? "Unknown")"
|
|
2434
2448
|
)
|
|
2435
|
-
|
|
2449
|
+
foundSides += 1
|
|
2436
2450
|
rightPeripheral = rightDevice
|
|
2437
2451
|
rightDevice.delegate = self
|
|
2438
2452
|
centralManager!.connect(
|
|
@@ -2445,7 +2459,8 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
|
2445
2459
|
}
|
|
2446
2460
|
}
|
|
2447
2461
|
|
|
2448
|
-
|
|
2462
|
+
// Continue scanning if only one matching cached half was available.
|
|
2463
|
+
return foundSides == 2
|
|
2449
2464
|
}
|
|
2450
2465
|
|
|
2451
2466
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum G1ConnectionTarget {
|
|
4
|
+
/// A legacy UUID needs a matching peripheral name; newly recorded UUIDs can
|
|
5
|
+
/// also reconnect without a name because their search ID was saved with them.
|
|
6
|
+
static func matchesCachedPeripheral(name: String?, searchID: String, cachedSearchID: String?) -> Bool {
|
|
7
|
+
guard searchID != "NOT_SET", searchID.count > 2 else { return false }
|
|
8
|
+
if let name, !name.isEmpty {
|
|
9
|
+
return name.contains("G1" + searchID)
|
|
10
|
+
}
|
|
11
|
+
return cachedSearchID == searchID
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -133,6 +133,15 @@ class Mach1: UltraliteBaseViewController, SGCManager {
|
|
|
133
133
|
|
|
134
134
|
/// Store discovered peripherals by their identifier
|
|
135
135
|
private var discoveredPeripherals: [String: CBPeripheral] = [:]
|
|
136
|
+
private var selectedPeripheralID: String?
|
|
137
|
+
|
|
138
|
+
private var currentDeviceMatchesSelection: Bool {
|
|
139
|
+
guard let selectedPeripheralID else { return true }
|
|
140
|
+
let currentDevice = UltraliteManager.shared.currentDevice
|
|
141
|
+
return VuzixConnectionTarget.matches(
|
|
142
|
+
name: currentDevice?.peripheral?.name ?? currentDevice?.getName(), target: selectedPeripheralID
|
|
143
|
+
)
|
|
144
|
+
}
|
|
136
145
|
|
|
137
146
|
private var textHandle: Int?
|
|
138
147
|
private var tapTextHandle: Int?
|
|
@@ -146,6 +155,7 @@ class Mach1: UltraliteBaseViewController, SGCManager {
|
|
|
146
155
|
if setupDone { return }
|
|
147
156
|
isConnectedListener = BondListener(listener: { [weak self] value in
|
|
148
157
|
guard let self else { return }
|
|
158
|
+
guard !value || self.currentDeviceMatchesSelection else { return }
|
|
149
159
|
Bridge.log("MACH1: isConnectedListener: \(value)")
|
|
150
160
|
|
|
151
161
|
if value {
|
|
@@ -171,6 +181,7 @@ class Mach1: UltraliteBaseViewController, SGCManager {
|
|
|
171
181
|
|
|
172
182
|
batteryLevelListener = BondListener(listener: { [weak self] value in
|
|
173
183
|
guard let self else { return }
|
|
184
|
+
guard self.currentDeviceMatchesSelection else { return }
|
|
174
185
|
Bridge.log("MACH1: batteryLevelListener: \(value)")
|
|
175
186
|
batteryLevel = value
|
|
176
187
|
DeviceStore.shared.apply("glasses", "batteryLevel", value)
|
|
@@ -238,23 +249,22 @@ class Mach1: UltraliteBaseViewController, SGCManager {
|
|
|
238
249
|
|
|
239
250
|
// Extract the ID from the device name if it contains brackets
|
|
240
251
|
// e.g., "Vuzix Z100 [f1b87c]" -> "f1b87c"
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
peripheralId = String(deviceId)
|
|
244
|
-
}
|
|
252
|
+
let peripheralId = VuzixConnectionTarget.identifier(id)
|
|
253
|
+
selectedPeripheralID = peripheralId
|
|
245
254
|
|
|
246
255
|
let isLinked = UltraliteManager.shared.isLinked.value
|
|
247
256
|
let currentDevice = UltraliteManager.shared.currentDevice
|
|
248
257
|
let isConnected =
|
|
249
258
|
isLinked && currentDevice != nil && currentDevice!.isPaired
|
|
250
259
|
&& currentDevice!.isConnected.value
|
|
251
|
-
let
|
|
260
|
+
let matchesSelection = currentDeviceMatchesSelection
|
|
261
|
+
let peripheral = discoveredPeripherals[peripheralId] ?? (matchesSelection ? currentDevice?.peripheral : nil)
|
|
252
262
|
|
|
253
263
|
// Bind listeners to get notified when device connects
|
|
254
264
|
UltraliteManager.shared.currentDevice?.isConnected.bind(listener: isConnectedListener!)
|
|
255
265
|
UltraliteManager.shared.currentDevice?.batteryLevel.bind(listener: batteryLevelListener!)
|
|
256
266
|
|
|
257
|
-
if isConnected {
|
|
267
|
+
if isConnected && matchesSelection {
|
|
258
268
|
// Already connected, request control now
|
|
259
269
|
let gotControl = currentDevice?.requestControl(
|
|
260
270
|
layout: UltraliteSDK.Ultralite.Layout.textBottomLeftAlign, timeout: 0,
|
|
@@ -266,7 +276,7 @@ class Mach1: UltraliteBaseViewController, SGCManager {
|
|
|
266
276
|
return
|
|
267
277
|
}
|
|
268
278
|
|
|
269
|
-
if !isLinked {
|
|
279
|
+
if !isLinked || !matchesSelection {
|
|
270
280
|
if peripheral == nil {
|
|
271
281
|
Bridge.log("Mach1Manager: No peripheral found or stored with ID: \(peripheralId)")
|
|
272
282
|
CONNECTING_DEVICE = peripheralId
|
|
@@ -279,6 +289,8 @@ class Mach1: UltraliteBaseViewController, SGCManager {
|
|
|
279
289
|
UltraliteManager.shared.stopScan()
|
|
280
290
|
CONNECTING_DEVICE = ""
|
|
281
291
|
|
|
292
|
+
// Replace the SDK link only after finding the explicitly selected device.
|
|
293
|
+
if isLinked { UltraliteManager.shared.unlink() }
|
|
282
294
|
UltraliteManager.shared.link(device: peripheral!, callback: linked)
|
|
283
295
|
UltraliteManager.shared.currentDevice?.isConnected.bind(listener: isConnectedListener!)
|
|
284
296
|
UltraliteManager.shared.currentDevice?.batteryLevel.bind(
|
|
@@ -3016,7 +3016,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3016
3016
|
|
|
3017
3017
|
case "stream_controller_probe":
|
|
3018
3018
|
if let response = StreamControllerProbe.response(json) {
|
|
3019
|
-
|
|
3019
|
+
// BES buffers non-waking commands when MTK enters standby, even while
|
|
3020
|
+
// its streaming CPU lease is held. The current probe needs a live reply.
|
|
3021
|
+
sendJson(response, wakeUp: true)
|
|
3020
3022
|
}
|
|
3021
3023
|
|
|
3022
3024
|
case "pong":
|
|
@@ -5284,7 +5286,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5284
5286
|
if let requestId {
|
|
5285
5287
|
json["request_id"] = requestId
|
|
5286
5288
|
}
|
|
5287
|
-
|
|
5289
|
+
// Wake ASG so the version request and its response can finish after idle.
|
|
5290
|
+
sendJson(json, wakeUp: true)
|
|
5288
5291
|
}
|
|
5289
5292
|
|
|
5290
5293
|
private func sendCoreTokenToAsgClient() {
|
|
@@ -994,7 +994,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
994
994
|
// global "last" one. peripheralToConnectName == nil means an auto-reconnect with no
|
|
995
995
|
// specific target, so the cached device is exactly what we want — keep using it.)
|
|
996
996
|
if let targetName = peripheralToConnectName,
|
|
997
|
-
|
|
997
|
+
peripheralToConnect.name != targetName
|
|
998
998
|
{
|
|
999
999
|
Bridge.log(
|
|
1000
1000
|
"NEX-CONN: 🔵 Stored UUID is '\(peripheralToConnect.name ?? "unnamed")' but target is '\(targetName)'. Skipping UUID fast-path; will scan for the target."
|
|
@@ -1088,7 +1088,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1088
1088
|
])
|
|
1089
1089
|
if let targetName = peripheralToConnectName,
|
|
1090
1090
|
let existingPeripheral = connectedPeripherals.first(where: {
|
|
1091
|
-
$0.name
|
|
1091
|
+
$0.name == targetName
|
|
1092
1092
|
})
|
|
1093
1093
|
{
|
|
1094
1094
|
Bridge.log(
|
|
@@ -2724,35 +2724,12 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2724
2724
|
// Always emit the discovered device for the UI list
|
|
2725
2725
|
emitDiscoveredDevice(deviceName)
|
|
2726
2726
|
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
shouldConnect = true
|
|
2734
|
-
connectionReason = "Target device name match: \(targetName)"
|
|
2735
|
-
}
|
|
2736
|
-
// During a user-initiated discovery scan, only list devices — never auto-connect a
|
|
2737
|
-
// saved/preferred device, so the user can pick a different one.
|
|
2738
|
-
else if isDiscoveryScan {
|
|
2739
|
-
shouldConnect = false
|
|
2740
|
-
}
|
|
2741
|
-
// Check if this matches our saved device for reconnection
|
|
2742
|
-
else if let savedName = savedDeviceName, deviceName == savedName {
|
|
2743
|
-
shouldConnect = true
|
|
2744
|
-
connectionReason = "Saved device reconnection: \(savedName)"
|
|
2745
|
-
}
|
|
2746
|
-
// Check if this matches preferred device ID
|
|
2747
|
-
else if let preferredId = preferredDeviceId {
|
|
2748
|
-
if let extractedId = extractDeviceId(from: deviceName), extractedId == preferredId {
|
|
2749
|
-
shouldConnect = true
|
|
2750
|
-
connectionReason = "Preferred device ID match: \(preferredId)"
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
if shouldConnect {
|
|
2755
|
-
connectToFoundDevice(peripheral, reason: connectionReason)
|
|
2727
|
+
if NexConnectionTarget.shouldConnect(
|
|
2728
|
+
name: deviceName, target: peripheralToConnectName, discoveryOnly: isDiscoveryScan,
|
|
2729
|
+
savedName: savedDeviceName, preferredID: preferredDeviceId,
|
|
2730
|
+
discoveredID: extractDeviceId(from: deviceName)
|
|
2731
|
+
) {
|
|
2732
|
+
connectToFoundDevice(peripheral, reason: "Selected target or saved reconnect identity")
|
|
2756
2733
|
}
|
|
2757
2734
|
}
|
|
2758
2735
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum NexConnectionTarget {
|
|
4
|
+
static func shouldConnect(
|
|
5
|
+
name: String, target: String?, discoveryOnly: Bool,
|
|
6
|
+
savedName: String?, preferredID: String?, discoveredID: String?
|
|
7
|
+
) -> Bool {
|
|
8
|
+
guard !discoveryOnly else { return false }
|
|
9
|
+
// An explicit selection is authoritative even when it does not match.
|
|
10
|
+
// Saved-device fallbacks are only for reconnecting without a selection.
|
|
11
|
+
if let target { return !target.isEmpty && name == target }
|
|
12
|
+
if let savedName, !savedName.isEmpty, name == savedName { return true }
|
|
13
|
+
return preferredID != nil && preferredID == discoveredID
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum VuzixConnectionTarget {
|
|
4
|
+
static func identifier(_ name: String) -> String {
|
|
5
|
+
guard let start = name.lastIndex(of: "["),
|
|
6
|
+
let end = name[start...].firstIndex(of: "]")
|
|
7
|
+
else { return name }
|
|
8
|
+
return String(name[name.index(after: start) ..< end])
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static func matches(name: String?, target: String) -> Bool {
|
|
12
|
+
guard let name, !target.isEmpty else { return false }
|
|
13
|
+
return identifier(name) == identifier(target)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
final class DeviceIdentityPairingTests: XCTestCase {
|
|
6
|
+
func testReadinessPromotesCoherentIdentityAndPreservesSameDeviceReconnects() {
|
|
7
|
+
let store = DeviceStore.shared.store
|
|
8
|
+
let saved = ["bluetooth", "glasses"].map { ($0, store.getCategory($0)) }
|
|
9
|
+
defer {
|
|
10
|
+
for (category, values) in saved {
|
|
11
|
+
for key in store.getCategory(category).keys where values[key] == nil {
|
|
12
|
+
store.remove(category, key)
|
|
13
|
+
}
|
|
14
|
+
for (key, value) in values {
|
|
15
|
+
store.set(category, key, value)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let cases = [
|
|
20
|
+
// pending name, pending address, saved model, expected address
|
|
21
|
+
("new", "", "test", ""),
|
|
22
|
+
("new", "new-address", "test", "new-address"),
|
|
23
|
+
("old", "", "test", "old-address"),
|
|
24
|
+
("", "", "test", "old-address"),
|
|
25
|
+
("old", "", "other-model", ""),
|
|
26
|
+
]
|
|
27
|
+
for (pendingName, pendingAddress, savedModel, expectedAddress) in cases {
|
|
28
|
+
let manager = DeviceManager()
|
|
29
|
+
let device = Simulated()
|
|
30
|
+
device.type = "test"
|
|
31
|
+
manager.sgc = device
|
|
32
|
+
defer {
|
|
33
|
+
manager.disconnect()
|
|
34
|
+
manager.cleanup()
|
|
35
|
+
}
|
|
36
|
+
store.set("bluetooth", "default_wearable", savedModel)
|
|
37
|
+
store.set("bluetooth", "device_name", "old")
|
|
38
|
+
store.set("bluetooth", "device_address", "old-address")
|
|
39
|
+
store.set("bluetooth", "pending_device_name", pendingName)
|
|
40
|
+
store.set("bluetooth", "pending_device_address", pendingAddress)
|
|
41
|
+
store.set("bluetooth", "shouldSendBootingMessage", false)
|
|
42
|
+
manager.handleDeviceReady()
|
|
43
|
+
XCTAssertEqual(DeviceStore.shared.get("bluetooth", "device_name") as? String, pendingName.isEmpty ? "old" : pendingName)
|
|
44
|
+
XCTAssertEqual(DeviceStore.shared.get("bluetooth", "device_address") as? String, expectedAddress)
|
|
45
|
+
XCTAssertEqual(DeviceStore.shared.get("bluetooth", "pending_device_name") as? String, "")
|
|
46
|
+
XCTAssertEqual(DeviceStore.shared.get("bluetooth", "pending_device_address") as? String, "")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class SGCConnectionTargetTests: XCTestCase {
|
|
5
|
+
func testG1RejectsAnotherPairsLegacyUUID() {
|
|
6
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: "Even G1_12_L_123456", searchID: "_47_", cachedSearchID: nil))
|
|
7
|
+
XCTAssertTrue(G1ConnectionTarget.matchesCachedPeripheral(name: "Even G1_47_L_123456", searchID: "_47_", cachedSearchID: nil))
|
|
8
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: "Even G1_470_R_123456", searchID: "_47_", cachedSearchID: nil))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func testG1NamelessBackgroundReconnectRequiresRecordedTarget() {
|
|
12
|
+
XCTAssertTrue(G1ConnectionTarget.matchesCachedPeripheral(name: nil, searchID: "_47_", cachedSearchID: "_47_"))
|
|
13
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: nil, searchID: "_47_", cachedSearchID: "_12_"))
|
|
14
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: nil, searchID: "_47_", cachedSearchID: nil))
|
|
15
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: "Even G1_12_L_123456", searchID: "_47_", cachedSearchID: "_47_"))
|
|
16
|
+
XCTAssertFalse(G1ConnectionTarget.matchesCachedPeripheral(name: nil, searchID: "NOT_SET", cachedSearchID: "NOT_SET"))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testNexExplicitSelectionExcludesSavedAndPreferredFallbacks() {
|
|
20
|
+
XCTAssertFalse(nex(name: "Nex1-old", target: "Nex1-new"))
|
|
21
|
+
XCTAssertFalse(nex(name: "Nex1-new-extra", target: "Nex1-new"))
|
|
22
|
+
XCTAssertTrue(nex(name: "Nex1-new", target: "Nex1-new"))
|
|
23
|
+
XCTAssertFalse(nex(name: "Nex1-old", target: ""))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func testNexDiscoveryNeverConnectsAndSavedReconnectStillWorks() {
|
|
27
|
+
XCTAssertFalse(nex(name: "Nex1-old", target: nil, discovery: true))
|
|
28
|
+
XCTAssertFalse(nex(name: "Nex1-new", target: "Nex1-new", discovery: true))
|
|
29
|
+
XCTAssertTrue(nex(name: "Nex1-old", target: nil))
|
|
30
|
+
XCTAssertTrue(nex(name: "Nex1-renamed", target: nil))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func testVuzixCurrentLinkMustMatchTheSelectedBracketedID() {
|
|
34
|
+
XCTAssertTrue(VuzixConnectionTarget.matches(name: "Vuzix Z100 [abc123]", target: "abc123"))
|
|
35
|
+
XCTAssertTrue(VuzixConnectionTarget.matches(name: "Vuzix Z100 [abc123]", target: "Vuzix Z100 [abc123]"))
|
|
36
|
+
XCTAssertFalse(VuzixConnectionTarget.matches(name: "Vuzix Z100 [old123]", target: "abc123"))
|
|
37
|
+
XCTAssertFalse(VuzixConnectionTarget.matches(name: nil, target: "abc123"))
|
|
38
|
+
XCTAssertFalse(VuzixConnectionTarget.matches(name: "", target: ""))
|
|
39
|
+
XCTAssertEqual(VuzixConnectionTarget.identifier("abc123"), "abc123")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func nex(name: String, target: String?, discovery: Bool = false) -> Bool {
|
|
43
|
+
NexConnectionTarget.shouldConnect(
|
|
44
|
+
name: name, target: target, discoveryOnly: discovery,
|
|
45
|
+
savedName: "Nex1-old", preferredID: "old", discoveredID: "old"
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|