@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.2.1

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.
Files changed (133) hide show
  1. package/README.md +4 -4
  2. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
  3. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +3 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
  10. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
  13. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +245 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
  29. package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
  31. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
  37. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
  38. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
  39. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
  40. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
  41. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
  42. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
  43. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +220 -0
  45. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
  46. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
  47. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
  48. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
  49. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
  50. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
  51. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
  52. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
  53. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
  54. package/build/BluetoothSdk.types.d.ts +13 -0
  55. package/build/BluetoothSdk.types.d.ts.map +1 -1
  56. package/build/BluetoothSdk.types.js +1 -0
  57. package/build/BluetoothSdk.types.js.map +1 -1
  58. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  59. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  60. package/build/_private/BluetoothSdkModule.js +3 -0
  61. package/build/_private/BluetoothSdkModule.js.map +1 -1
  62. package/build/generated/changelogCatalog.d.ts +2 -2
  63. package/build/generated/changelogCatalog.js +2 -2
  64. package/build/generated/changelogCatalog.js.map +1 -1
  65. package/build/generated/releaseMetadata.js +5 -5
  66. package/build/generated/releaseMetadata.js.map +1 -1
  67. package/build/index.d.ts.map +1 -1
  68. package/build/index.js +1 -0
  69. package/build/index.js.map +1 -1
  70. package/ios/BluetoothSdkModule.swift +9 -0
  71. package/ios/MentraBluetoothSDK.podspec +3 -2
  72. package/ios/Source/BluetoothBackgroundMode.swift +32 -0
  73. package/ios/Source/BluetoothSdkDefaults.swift +6 -1
  74. package/ios/Source/Bridge.swift +15 -2
  75. package/ios/Source/DeviceManager.swift +96 -63
  76. package/ios/Source/DeviceStore.swift +26 -7
  77. package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
  78. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  79. package/ios/Source/MentraBluetoothSDK.swift +18 -0
  80. package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
  81. package/ios/Source/requests/DisplayRequests.swift +1 -1
  82. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
  83. package/ios/Source/sgcs/Ar99.swift +1 -2
  84. package/ios/Source/sgcs/G1.swift +68 -38
  85. package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
  86. package/ios/Source/sgcs/G2.swift +261 -247
  87. package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
  88. package/ios/Source/sgcs/Mach1.swift +19 -7
  89. package/ios/Source/sgcs/MentraLive.swift +201 -156
  90. package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
  91. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
  92. package/ios/Source/sgcs/MentraNex.swift +8 -31
  93. package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
  94. package/ios/Source/sgcs/Nimo.swift +280 -408
  95. package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
  96. package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
  97. package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
  98. package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
  99. package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
  100. package/ios/Source/sgcs/SGCManager.swift +24 -0
  101. package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
  102. package/ios/Source/status/DeviceStatus.swift +1 -0
  103. package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
  104. package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
  105. package/ios/Source/utils/G2Text.swift +11 -0
  106. package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
  107. package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
  108. package/ios/Tests/DashboardRenderingTests.swift +31 -0
  109. package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
  110. package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
  111. package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
  112. package/ios/Tests/G1PairingTests.swift +162 -0
  113. package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
  114. package/ios/Tests/G2TextTests.swift +10 -0
  115. package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
  116. package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
  117. package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
  118. package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
  119. package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
  120. package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
  121. package/ios/Tests/NimoDiscoveryTests.swift +79 -0
  122. package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
  123. package/ios/Tests/VersionInfoCommandTests.swift +53 -0
  124. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
  125. package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
  126. package/package.json +2 -2
  127. package/plugin/build/withIos.d.ts +2 -0
  128. package/plugin/build/withIos.js +12 -1
  129. package/src/BluetoothSdk.types.ts +14 -0
  130. package/src/_private/BluetoothSdkModule.ts +8 -0
  131. package/src/generated/changelogCatalog.ts +2 -2
  132. package/src/generated/releaseMetadata.ts +5 -5
  133. package/src/index.ts +1 -0
@@ -0,0 +1,101 @@
1
+ import Foundation
2
+
3
+ /// Readiness belongs to an authenticated pair and a completed initialization attempt, never to
4
+ /// a gesture from one arm. A generation retires asynchronous setup when either arm is lost.
5
+ struct G2ConnectionSession {
6
+ enum Side: String, CaseIterable {
7
+ case left = "L"
8
+ case right = "R"
9
+ }
10
+
11
+ private(set) var generation: UInt64 = 0
12
+ private var initialized: Set<Side> = []
13
+ private var authenticatedSides: Set<Side> = []
14
+ private var authStarted = false
15
+ private var setupComplete = false
16
+
17
+ var isReady: Bool {
18
+ initialized.count == 2 && authenticatedSides.count == 2 && setupComplete
19
+ }
20
+
21
+ func needsRecovery(attempt: UInt64) -> Bool {
22
+ generation == attempt && !isReady
23
+ }
24
+
25
+ mutating func initialize(_ side: Side) {
26
+ initialized.insert(side)
27
+ }
28
+
29
+ mutating func beginAuthentication() -> UInt64? {
30
+ guard initialized.count == 2, !authStarted else { return nil }
31
+ authStarted = true
32
+ return generation
33
+ }
34
+
35
+ mutating func authenticated(_ side: Side, success: Bool) {
36
+ guard authStarted else { return }
37
+ if success { authenticatedSides.insert(side) }
38
+ else { authenticatedSides.remove(side) }
39
+ }
40
+
41
+ mutating func completeSetup(generation: UInt64) {
42
+ guard self.generation == generation, authStarted else { return }
43
+ setupComplete = true
44
+ }
45
+
46
+ mutating func reset() {
47
+ generation &+= 1
48
+ initialized.removeAll()
49
+ authenticatedSides.removeAll()
50
+ authStarted = false
51
+ setupComplete = false
52
+ }
53
+ }
54
+
55
+ enum G2ConnectionTarget {
56
+ /// A serial-scoped cached UUID identifies an arm even without a new advertisement. Nearby
57
+ /// arms can share the G2_32 name fragment; only an advertisement's serial can select an
58
+ /// uncached arm. Never fall back to names alone or another pair's cached identifiers.
59
+ static func side(
60
+ identifier: UUID, name: String?, advertisedSerial: String?, searchID: String,
61
+ leftUUID: UUID?, rightUUID: UUID?
62
+ ) -> G2ConnectionSession.Side? {
63
+ guard !searchID.isEmpty, searchID != "NOT_SET" else { return nil }
64
+ if identifier == leftUUID, identifier == rightUUID { return nil }
65
+ if identifier == leftUUID { return .left }
66
+ if identifier == rightUUID { return .right }
67
+ guard let advertisedSerial, advertisedSerial.contains(searchID), let name else { return nil }
68
+ if name.contains("_L_") { return .left }
69
+ if name.contains("_R_") { return .right }
70
+ return nil
71
+ }
72
+ }
73
+
74
+ /// Normal pairing connects the arms sequentially. Surface a missing arm only after the
75
+ /// same partial link has persisted for three seconds, using monotonic time.
76
+ struct G2PartialConnection {
77
+ static let noticeDelay: TimeInterval = 3
78
+ private(set) var missingSide: G2ConnectionSession.Side?
79
+ private var partialSince: TimeInterval = 0
80
+
81
+ mutating func update(leftConnected: Bool, rightConnected: Bool, now: TimeInterval) {
82
+ let missing: G2ConnectionSession.Side? = leftConnected == rightConnected
83
+ ? nil : (leftConnected ? .right : .left)
84
+ if missing != missingSide {
85
+ missingSide = missing
86
+ partialSince = now
87
+ }
88
+ }
89
+
90
+ func visibleMissingSide(now: TimeInterval) -> G2ConnectionSession.Side? {
91
+ now - partialSince >= Self.noticeDelay ? missingSide : nil
92
+ }
93
+
94
+ var timeoutError: String {
95
+ switch missingSide {
96
+ case .left: "errors:g2LeftArmUnavailable"
97
+ case .right: "errors:g2RightArmUnavailable"
98
+ case nil: "errors:g2ConnectionTimedOut"
99
+ }
100
+ }
101
+ }
@@ -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
- var peripheralId = id
242
- if let deviceId = id.split(separator: "[").last?.split(separator: "]").first {
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 peripheral = discoveredPeripherals[peripheralId] ?? currentDevice?.peripheral
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(