@mentra/bluetooth-sdk 0.1.18 → 0.1.20

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 (77) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +135 -51
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +120 -17
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -6
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -21
  6. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +13 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +551 -159
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
  17. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
  18. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -5
  20. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
  22. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +109 -73
  24. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
  27. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -9
  29. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
  31. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  36. package/build/BluetoothSdk.types.d.ts +37 -1
  37. package/build/BluetoothSdk.types.d.ts.map +1 -1
  38. package/build/BluetoothSdk.types.js.map +1 -1
  39. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  40. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  41. package/build/_private/BluetoothSdkModule.js.map +1 -1
  42. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  43. package/build/_private/photoRequestPayload.js +1 -0
  44. package/build/_private/photoRequestPayload.js.map +1 -1
  45. package/build/index.d.ts +1 -1
  46. package/build/index.d.ts.map +1 -1
  47. package/build/index.js +5 -0
  48. package/build/index.js.map +1 -1
  49. package/build/types/index.d.ts +3 -0
  50. package/build/types/index.d.ts.map +1 -0
  51. package/build/types/index.js +2 -0
  52. package/build/types/index.js.map +1 -0
  53. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  54. package/ios/Source/DeviceManager.swift +136 -30
  55. package/ios/Source/DeviceStore.swift +5 -5
  56. package/ios/Source/MentraBluetoothSDK.swift +40 -24
  57. package/ios/Source/OtaManifest.swift +1 -1
  58. package/ios/Source/camera/CameraModels.swift +17 -3
  59. package/ios/Source/sgcs/G1.swift +1 -1
  60. package/ios/Source/sgcs/G2.swift +518 -11
  61. package/ios/Source/sgcs/MentraLive.swift +443 -42
  62. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  63. package/ios/Source/sgcs/MentraNex.swift +416 -32
  64. package/ios/Source/sgcs/SGCManager.swift +155 -0
  65. package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
  66. package/ios/Source/status/DeviceStatus.swift +0 -8
  67. package/ios/Source/utils/BleJsonCompact.swift +395 -0
  68. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  69. package/ios/Source/utils/G1Text.swift +30 -1
  70. package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
  71. package/ios/Source/utils/MessageChunker.swift +73 -0
  72. package/package.json +14 -7
  73. package/src/BluetoothSdk.types.ts +39 -1
  74. package/src/_private/BluetoothSdkModule.ts +4 -0
  75. package/src/_private/photoRequestPayload.ts +1 -0
  76. package/src/index.ts +6 -0
  77. package/src/types/index.ts +7 -0
@@ -0,0 +1,183 @@
1
+ import CoreBluetooth
2
+ import Foundation
3
+
4
+ /// Read-only LE L2CAP CoC fast path for Mentra Live file transfers.
5
+ ///
6
+ /// BES sends the same K900 file frames over this channel that it otherwise
7
+ /// sends through the FILE_READ GATT characteristic. The stream is drained on
8
+ /// a dedicated thread so CoreBluetooth credits are returned independently of
9
+ /// React Native and main-thread work.
10
+ final class MentraLiveL2capChannel: NSObject, StreamDelegate {
11
+ private static let readChunkSize = 4096
12
+ private static let maxBufferSize = 64 * 1024
13
+ private static let frameHeaderSize = 5
14
+ private static let frameOverhead = 32
15
+ private static let maxFilePayloadSize = K900ProtocolUtils.FILE_PACK_SIZE
16
+
17
+ private let channel: CBL2CAPChannel
18
+ private let onFileFrame: (Data) -> Void
19
+ private let onClose: () -> Void
20
+ private let stateLock = NSLock()
21
+
22
+ private var receiveBuffer = [UInt8]()
23
+ private var worker: Thread?
24
+ private var closed = false
25
+ private var closeNotified = false
26
+
27
+ init(
28
+ channel: CBL2CAPChannel,
29
+ onFileFrame: @escaping (Data) -> Void,
30
+ onClose: @escaping () -> Void
31
+ ) {
32
+ self.channel = channel
33
+ self.onFileFrame = onFileFrame
34
+ self.onClose = onClose
35
+ super.init()
36
+ }
37
+
38
+ func start() {
39
+ guard worker == nil else { return }
40
+ let thread = Thread { [weak self] in
41
+ self?.runReadLoop()
42
+ }
43
+ thread.name = "MentraLive-L2CAP"
44
+ thread.qualityOfService = .userInitiated
45
+ worker = thread
46
+ thread.start()
47
+ }
48
+
49
+ func close() {
50
+ stateLock.lock()
51
+ let wasClosed = closed
52
+ closed = true
53
+ stateLock.unlock()
54
+
55
+ guard !wasClosed else { return }
56
+ channel.inputStream?.close()
57
+ channel.outputStream?.close()
58
+ }
59
+
60
+ private var isClosed: Bool {
61
+ stateLock.lock()
62
+ defer { stateLock.unlock() }
63
+ return closed
64
+ }
65
+
66
+ private func runReadLoop() {
67
+ guard let input = channel.inputStream, let output = channel.outputStream else {
68
+ notifyClosed()
69
+ return
70
+ }
71
+
72
+ input.delegate = self
73
+ input.schedule(in: .current, forMode: .default)
74
+ output.schedule(in: .current, forMode: .default)
75
+ input.open()
76
+ output.open()
77
+
78
+ while !isClosed {
79
+ _ = RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 0.1))
80
+ }
81
+
82
+ input.close()
83
+ output.close()
84
+ input.remove(from: .current, forMode: .default)
85
+ output.remove(from: .current, forMode: .default)
86
+ input.delegate = nil
87
+ notifyClosed()
88
+ }
89
+
90
+ func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
91
+ guard aStream === channel.inputStream else { return }
92
+
93
+ switch eventCode {
94
+ case .hasBytesAvailable:
95
+ drainInput()
96
+ case .endEncountered, .errorOccurred:
97
+ close()
98
+ default:
99
+ break
100
+ }
101
+ }
102
+
103
+ private func drainInput() {
104
+ guard let input = channel.inputStream else { return }
105
+ var chunk = [UInt8](repeating: 0, count: Self.readChunkSize)
106
+
107
+ while input.hasBytesAvailable, !isClosed {
108
+ let count = input.read(&chunk, maxLength: chunk.count)
109
+ if count > 0 {
110
+ appendAndDispatch(Array(chunk.prefix(count)))
111
+ } else if count < 0 {
112
+ close()
113
+ return
114
+ } else {
115
+ break
116
+ }
117
+ }
118
+ }
119
+
120
+ private func appendAndDispatch(_ bytes: [UInt8]) {
121
+ if receiveBuffer.count + bytes.count > Self.maxBufferSize {
122
+ receiveBuffer.removeAll(keepingCapacity: true)
123
+ }
124
+ guard bytes.count <= Self.maxBufferSize else { return }
125
+ receiveBuffer.append(contentsOf: bytes)
126
+
127
+ while true {
128
+ guard let start = startMarkerIndex() else {
129
+ if receiveBuffer.last == 0x23 {
130
+ receiveBuffer = [0x23]
131
+ } else {
132
+ receiveBuffer.removeAll(keepingCapacity: true)
133
+ }
134
+ return
135
+ }
136
+
137
+ if start > 0 {
138
+ receiveBuffer.removeFirst(start)
139
+ }
140
+ guard receiveBuffer.count >= Self.frameHeaderSize else { return }
141
+
142
+ let payloadSize = (Int(receiveBuffer[3]) << 8) | Int(receiveBuffer[4])
143
+ guard payloadSize <= Self.maxFilePayloadSize else {
144
+ receiveBuffer.removeFirst()
145
+ continue
146
+ }
147
+
148
+ let frameSize = Self.frameOverhead + payloadSize
149
+ guard receiveBuffer.count >= frameSize else { return }
150
+
151
+ guard receiveBuffer[frameSize - 2] == 0x24,
152
+ receiveBuffer[frameSize - 1] == 0x24
153
+ else {
154
+ receiveBuffer.removeFirst()
155
+ continue
156
+ }
157
+
158
+ onFileFrame(Data(receiveBuffer.prefix(frameSize)))
159
+ receiveBuffer.removeFirst(frameSize)
160
+ }
161
+ }
162
+
163
+ private func startMarkerIndex() -> Int? {
164
+ guard receiveBuffer.count >= 2 else { return nil }
165
+ for index in 0 ..< (receiveBuffer.count - 1)
166
+ where receiveBuffer[index] == 0x23 && receiveBuffer[index + 1] == 0x23
167
+ {
168
+ return index
169
+ }
170
+ return nil
171
+ }
172
+
173
+ private func notifyClosed() {
174
+ stateLock.lock()
175
+ guard !closeNotified else {
176
+ stateLock.unlock()
177
+ return
178
+ }
179
+ closeNotified = true
180
+ stateLock.unlock()
181
+ onClose()
182
+ }
183
+ }