@mentra/bluetooth-sdk 3.2.1-dev.311 → 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,137 @@
1
+ import Foundation
2
+
3
+ struct MentraLiveFileTransferSession {
4
+ let fileName: String
5
+ let fileSize: Int // NOTE: May be "fake" (inflated) due to BES firmware workaround
6
+ let dynamicPayload: Bool
7
+ var actualPackSize: Int = 0 // Actual pack size from first received packet
8
+ var totalPackets: Int
9
+ var expectedNextPacket: Int = 0
10
+ var receivedPackets: [Int: Data] = [:]
11
+ let startTime: Date
12
+ var isComplete: Bool = false
13
+ var isAnnounced: Bool = false
14
+
15
+ /// BES2700 firmware hardcodes FILE_PACK_SIZE=400 when calculating totalPack.
16
+ /// Android glasses "lie" about fileSize to make BES expect correct packet count.
17
+ private static let BES_HARDCODED_PACK_SIZE = 400
18
+
19
+ init(fileName: String, fileSize: Int, announcedPackets: Int? = nil, flags: UInt16 = 0) {
20
+ self.fileName = fileName
21
+ self.fileSize = fileSize
22
+ dynamicPayload = (flags & 0x0002) != 0
23
+ let computedPackets =
24
+ (fileSize + K900ProtocolUtils.FILE_PACK_SIZE - 1) / K900ProtocolUtils.FILE_PACK_SIZE
25
+ if let announced = announcedPackets, announced > 0 {
26
+ totalPackets = announced
27
+ isAnnounced = true
28
+ } else {
29
+ totalPackets = computedPackets
30
+ isAnnounced = false
31
+ }
32
+ startTime = Date()
33
+ }
34
+
35
+ mutating func updateAnnouncedPackets(_ announced: Int) {
36
+ guard announced > 0 else { return }
37
+ totalPackets = announced
38
+ isAnnounced = true
39
+ if expectedNextPacket >= totalPackets {
40
+ expectedNextPacket = min(expectedNextPacket, max(totalPackets - 1, 0))
41
+ }
42
+ }
43
+
44
+ /// Recalculate total packets based on actual pack size from received packet.
45
+ /// Dynamic headers carry the true byte count. Only legacy small packs use inflated sizes.
46
+ mutating func recalculateTotalPackets(actualPackSize: Int) {
47
+ guard actualPackSize > 0, actualPackSize <= K900ProtocolUtils.FILE_PACK_SIZE else { return }
48
+
49
+ self.actualPackSize = actualPackSize
50
+
51
+ // Detect BES lie: if fileSize is exact multiple of 400, glasses used the lie strategy
52
+ let isBesLie = !dynamicPayload
53
+ && (fileSize % Self.BES_HARDCODED_PACK_SIZE == 0)
54
+ && (actualPackSize < Self.BES_HARDCODED_PACK_SIZE)
55
+
56
+ let newTotalPackets: Int
57
+ if isBesLie {
58
+ // BES lie detected: totalPackets = fileSize / 400
59
+ newTotalPackets = fileSize / Self.BES_HARDCODED_PACK_SIZE
60
+ Bridge.log(
61
+ "📦 BES Lie detected! fakeFileSize=\(fileSize), totalPackets=\(newTotalPackets), actualPackSize=\(actualPackSize)"
62
+ )
63
+ } else {
64
+ // Normal case: calculate based on actual pack size
65
+ newTotalPackets = (fileSize + actualPackSize - 1) / actualPackSize
66
+ }
67
+
68
+ if newTotalPackets != totalPackets {
69
+ Bridge.log(
70
+ "📦 Recalculating totalPackets: \(totalPackets) -> \(newTotalPackets) (packSize=\(actualPackSize), fileSize=\(fileSize))"
71
+ )
72
+ totalPackets = newTotalPackets
73
+ }
74
+ }
75
+
76
+ mutating func addPacket(_ index: Int, data: Data) -> Bool {
77
+ guard index >= 0 else { return false }
78
+
79
+ // On first packet, recalculate total packets only when we do not already
80
+ // have an authoritative pack size from protocol metadata.
81
+ if receivedPackets.isEmpty && actualPackSize == 0 && !data.isEmpty {
82
+ recalculateTotalPackets(actualPackSize: data.count)
83
+ }
84
+
85
+ guard index < totalPackets else { return false }
86
+ if dynamicPayload {
87
+ let expectedSize = min(actualPackSize, fileSize - index * actualPackSize)
88
+ guard expectedSize > 0, data.count == expectedSize else { return false }
89
+ }
90
+
91
+ guard receivedPackets[index] == nil else {
92
+ return false
93
+ }
94
+
95
+ receivedPackets[index] = data
96
+
97
+ while receivedPackets[expectedNextPacket] != nil, expectedNextPacket < totalPackets {
98
+ expectedNextPacket += 1
99
+ }
100
+
101
+ isComplete = (receivedPackets.count == totalPackets)
102
+ return true
103
+ }
104
+
105
+ func isFinalPacket(_ index: Int) -> Bool {
106
+ index == totalPackets - 1
107
+ }
108
+
109
+ func missingPacketIndices() -> [Int] {
110
+ guard totalPackets > receivedPackets.count else { return [] }
111
+ return (0 ..< totalPackets).compactMap { receivedPackets[$0] == nil ? $0 : nil }
112
+ }
113
+
114
+ /// Assemble file from received packets.
115
+ /// NOTE: Calculates actual file size from received data, NOT from header fileSize,
116
+ /// because fileSize may be "fake" (inflated) due to BES firmware workaround.
117
+ func assembleFile() -> Data? {
118
+ guard isComplete else { return nil }
119
+
120
+ // Calculate actual file size by summing all received packet sizes
121
+ let actualFileSize = receivedPackets.values.reduce(0) { $0 + $1.count }
122
+
123
+ Bridge.log(
124
+ "📦 Assembling file: headerFileSize=\(fileSize), actualFileSize=\(actualFileSize), totalPackets=\(totalPackets)"
125
+ )
126
+
127
+ var fileData = Data(capacity: actualFileSize)
128
+
129
+ for i in 0 ..< totalPackets {
130
+ if let packet = receivedPackets[i] {
131
+ fileData.append(packet)
132
+ }
133
+ }
134
+
135
+ return fileData
136
+ }
137
+ }
@@ -61,4 +61,51 @@ enum MentraLiveConnectionAttemptPolicy {
61
61
  ) -> Bool {
62
62
  !pairingYieldActive && matchesActiveAttempt
63
63
  }
64
+
65
+ /// How long a pending reconnect reports CONNECTING before settling to DISCONNECTED. Matches
66
+ /// the span of the scan backoff it replaces (1+2+4+8+16 s, then 30 s × 5 attempts).
67
+ static let pendingReconnectSettleMs = 181_000
68
+
69
+ /// Whether an unexpected link loss should re-arm a pending connection to the same
70
+ /// peripheral. iOS completes a pending `connect` whenever the glasses advertise again,
71
+ /// even while the app is suspended (phone locked), and wakes the app to handle it. A scan
72
+ /// without service filters returns nothing in the background, and the backoff timers that
73
+ /// drive it stop while the app is suspended, so the scan path alone never reconnects there.
74
+ static func shouldReconnectDirectly(
75
+ isKilled: Bool,
76
+ pairingYieldActive: Bool,
77
+ bluetoothPoweredOn: Bool,
78
+ peripheralName: String?,
79
+ savedDeviceName: String?
80
+ ) -> Bool {
81
+ guard !isKilled, !pairingYieldActive, bluetoothPoweredOn,
82
+ let peripheralName, !peripheralName.isEmpty,
83
+ let savedDeviceName
84
+ else {
85
+ return false
86
+ }
87
+ return peripheralName == savedDeviceName
88
+ }
89
+ }
90
+
91
+ /// Ownership of the one armed pending reconnect. Each arm returns a new attempt number, so a
92
+ /// timer captured for an earlier attempt can tell it no longer owns the connection state after
93
+ /// a failure, a fallback connect or a re-arm.
94
+ struct MentraLivePendingReconnect {
95
+ private(set) var attempt = 0
96
+ private(set) var isArmed = false
97
+
98
+ mutating func arm() -> Int {
99
+ attempt += 1
100
+ isArmed = true
101
+ return attempt
102
+ }
103
+
104
+ mutating func clear() {
105
+ isArmed = false
106
+ }
107
+
108
+ func owns(_ candidate: Int) -> Bool {
109
+ isArmed && candidate == attempt
110
+ }
64
111
  }
@@ -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
- !(peripheralToConnect.name?.contains(targetName) ?? false)
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?.contains(targetName) == true
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
- // Auto-connect logic based on target or saved device (from Java MentraNexSGC)
2728
- var shouldConnect = false
2729
- var connectionReason = ""
2730
-
2731
- // Check if this matches our target device name for connection
2732
- if let targetName = peripheralToConnectName, deviceName.contains(targetName) {
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
+ }