@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.
- package/README.md +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +135 -51
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +120 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +551 -159
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +109 -73
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -9
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/build/BluetoothSdk.types.d.ts +37 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +1 -0
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -0
- package/build/index.js.map +1 -1
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +136 -30
- package/ios/Source/DeviceStore.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +40 -24
- package/ios/Source/OtaManifest.swift +1 -1
- package/ios/Source/camera/CameraModels.swift +17 -3
- package/ios/Source/sgcs/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +518 -11
- package/ios/Source/sgcs/MentraLive.swift +443 -42
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- package/ios/Source/sgcs/MentraNex.swift +416 -32
- package/ios/Source/sgcs/SGCManager.swift +155 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
- package/ios/Source/status/DeviceStatus.swift +0 -8
- package/ios/Source/utils/BleJsonCompact.swift +395 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/G1Text.swift +30 -1
- package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
- package/ios/Source/utils/MessageChunker.swift +73 -0
- package/package.json +14 -7
- package/src/BluetoothSdk.types.ts +39 -1
- package/src/_private/BluetoothSdkModule.ts +4 -0
- package/src/_private/photoRequestPayload.ts +1 -0
- package/src/index.ts +6 -0
- 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
|
+
}
|