@mentra/bluetooth-sdk 0.1.9 → 0.1.10
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 +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +241 -43
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +36 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +30 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +174 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +58 -19
- package/ios/Source/DeviceManager.swift +149 -17
- package/ios/Source/DeviceStore.swift +16 -6
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +6 -0
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +645 -403
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +812 -319
- package/ios/Source/sgcs/MentraNex.swift +412 -182
- package/ios/Source/sgcs/SGCManager.swift +57 -3
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +331 -59
- package/src/_private/BluetoothSdkModule.ts +113 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +58 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
|
@@ -18,11 +18,11 @@ extension Data {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/// Nex firmware expects tier 1–
|
|
21
|
+
/// Nex firmware expects tier 1–4 in protobuf `DisplayDistanceConfig.distance_cm` (name is legacy, not cm).
|
|
22
22
|
/// Keep in sync with `NexProtobufUtils.dashboardDepthToDistanceCm` (Android `NexSGCUtils.kt`).
|
|
23
23
|
enum NexDashboardDisplayWire {
|
|
24
24
|
static let depthMin = 1
|
|
25
|
-
static let depthMax =
|
|
25
|
+
static let depthMax = 4
|
|
26
26
|
|
|
27
27
|
static func depthToWireTier(_ depth: Int) -> UInt32 {
|
|
28
28
|
UInt32(min(max(depth, depthMin), depthMax))
|
|
@@ -42,11 +42,18 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
42
42
|
|
|
43
43
|
func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
|
|
44
44
|
|
|
45
|
-
func setMicEnabled(_: Bool) {
|
|
45
|
+
func setMicEnabled(_ enabled: Bool) {
|
|
46
|
+
shouldUseGlassesMic = enabled
|
|
47
|
+
if enabled {
|
|
48
|
+
startMicBeat()
|
|
49
|
+
} else {
|
|
50
|
+
stopMicBeat()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
46
53
|
|
|
47
54
|
func requestPhoto(
|
|
48
55
|
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
49
|
-
compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
56
|
+
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
50
57
|
) {}
|
|
51
58
|
|
|
52
59
|
func startStream(_: [String: Any]) {}
|
|
@@ -69,15 +76,35 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
69
76
|
|
|
70
77
|
func sendCameraFovSetting() {}
|
|
71
78
|
|
|
72
|
-
func setBrightness(_: Int, autoMode
|
|
79
|
+
func setBrightness(_ level: Int, autoMode: Bool) {
|
|
80
|
+
updateGlassesBrightness(level)
|
|
81
|
+
updateGlassesAutoBrightness(autoMode)
|
|
82
|
+
}
|
|
73
83
|
|
|
74
|
-
func sendDoubleTextWall(_: String, _: String) {
|
|
84
|
+
func sendDoubleTextWall(_ top: String, _ bottom: String) {
|
|
85
|
+
sendTextWall("\(top)\n\(bottom)")
|
|
86
|
+
}
|
|
75
87
|
|
|
76
|
-
func displayBitmap(base64ImageData _:
|
|
77
|
-
|
|
88
|
+
func displayBitmap(base64ImageData: String, x _: Int32? = nil, y _: Int32? = nil, width _: Int32? = nil, height _: Int32? = nil) async -> Bool {
|
|
89
|
+
guard let imageData = Data(base64Encoded: base64ImageData),
|
|
90
|
+
let image = UIImage(data: imageData)
|
|
91
|
+
else {
|
|
92
|
+
Bridge.log("NEX: Failed to decode base64 image payload")
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
guard let bmpData = convertUIImageToBmpData(image) else {
|
|
96
|
+
Bridge.log("NEX: Failed to convert UIImage to bitmap bytes")
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
let pixelWidth = image.cgImage?.width ?? Int(image.size.width * image.scale)
|
|
100
|
+
let pixelHeight = image.cgImage?.height ?? Int(image.size.height * image.scale)
|
|
101
|
+
displayBitmapData(bmpData, width: pixelWidth, height: pixelHeight)
|
|
102
|
+
return true
|
|
78
103
|
}
|
|
79
104
|
|
|
80
|
-
func showDashboard() {
|
|
105
|
+
func showDashboard() {
|
|
106
|
+
exit()
|
|
107
|
+
}
|
|
81
108
|
|
|
82
109
|
func setDashboardPosition(_ height: Int, _ depth: Int) {
|
|
83
110
|
// Same order as Android MentraNex: display_height then display_distance.
|
|
@@ -93,13 +120,19 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
93
120
|
updateGlassesDisplayDistance(depth: depth)
|
|
94
121
|
}
|
|
95
122
|
|
|
96
|
-
func setHeadUpAngle(_: Int) {
|
|
123
|
+
func setHeadUpAngle(_ angle: Int) {
|
|
124
|
+
updateGlassesHeadUpAngle(angle)
|
|
125
|
+
}
|
|
97
126
|
|
|
98
|
-
func getBatteryStatus() {
|
|
127
|
+
func getBatteryStatus() {
|
|
128
|
+
queryBatteryStatus()
|
|
129
|
+
}
|
|
99
130
|
|
|
100
131
|
func setSilentMode(_: Bool) {}
|
|
101
132
|
|
|
102
|
-
func exit() {
|
|
133
|
+
func exit() {
|
|
134
|
+
queueChunks([[0x18]], waitTimeMs: 100)
|
|
135
|
+
}
|
|
103
136
|
|
|
104
137
|
func sendShutdown() {}
|
|
105
138
|
|
|
@@ -110,17 +143,27 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
110
143
|
offDurationMs _: Int, count _: Int
|
|
111
144
|
) {}
|
|
112
145
|
|
|
113
|
-
func forget() {
|
|
146
|
+
func forget() {
|
|
147
|
+
destroy()
|
|
148
|
+
}
|
|
114
149
|
|
|
115
|
-
func connectById(_: String) {
|
|
150
|
+
func connectById(_ id: String) {
|
|
151
|
+
savePreferredDeviceId(id)
|
|
152
|
+
connect(name: id)
|
|
153
|
+
}
|
|
116
154
|
|
|
117
155
|
func getConnectedBluetoothName() -> String? {
|
|
118
|
-
|
|
156
|
+
peripheral?.name
|
|
119
157
|
}
|
|
120
158
|
|
|
121
|
-
func cleanup() {
|
|
159
|
+
func cleanup() {
|
|
160
|
+
destroy()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func ping() {
|
|
164
|
+
Bridge.log("NEX: ping() is host-side no-op for this transport")
|
|
165
|
+
}
|
|
122
166
|
|
|
123
|
-
func ping() {}
|
|
124
167
|
func connectController() {}
|
|
125
168
|
func disconnectController() {}
|
|
126
169
|
|
|
@@ -165,10 +208,11 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
165
208
|
private var isDisconnecting = false
|
|
166
209
|
private var reconnectionTimer: Timer?
|
|
167
210
|
private var reconnectionAttempts = 0
|
|
168
|
-
// TODO: change this
|
|
169
211
|
private let maxReconnectionAttempts = -1 // -1 for unlimited
|
|
170
|
-
private let reconnectionInterval: TimeInterval =
|
|
212
|
+
private let reconnectionInterval: TimeInterval = 2.0
|
|
171
213
|
private var peripheralToConnectName: String?
|
|
214
|
+
private let INITIAL_CONNECTION_DELAY_MS: UInt64 = 350
|
|
215
|
+
private let DELAY_BETWEEN_CHUNKS_SEND_MS: UInt64 = 10
|
|
172
216
|
|
|
173
217
|
// Heartbeat tracking (like Java implementation)
|
|
174
218
|
private var heartbeatCount = 0
|
|
@@ -180,6 +224,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
180
224
|
private var micBeatCount = 0
|
|
181
225
|
private let MICBEAT_INTERVAL_MS: TimeInterval = 30 * 60 // 30 minutes like Java
|
|
182
226
|
private var shouldUseGlassesMic = true
|
|
227
|
+
private var isMicrophoneEnabled = true
|
|
183
228
|
private var microphoneStateBeforeDisconnection = false
|
|
184
229
|
|
|
185
230
|
// Whitelist system (like Java implementation)
|
|
@@ -191,6 +236,13 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
191
236
|
|
|
192
237
|
/// Device discovery cache (like MentraLive)
|
|
193
238
|
private var discoveredPeripherals = [String: CBPeripheral]() // name -> peripheral
|
|
239
|
+
private var lastConnectionTimestamp: TimeInterval = 0
|
|
240
|
+
private var lastReceivedLc3Sequence = -1
|
|
241
|
+
private var currentImageChunks: [[UInt8]] = []
|
|
242
|
+
private var isImageSendProgressing = false
|
|
243
|
+
private var servicesReady = false
|
|
244
|
+
private var serviceReadyWaiters: [CheckedContinuation<Void, Never>] = []
|
|
245
|
+
private var pendingWriteContinuation: CheckedContinuation<Void, Never>?
|
|
194
246
|
|
|
195
247
|
// MARK: - Published Properties (G1-compatible)
|
|
196
248
|
|
|
@@ -271,9 +323,10 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
271
323
|
// MARK: - Singleton Access
|
|
272
324
|
|
|
273
325
|
@objc static func getInstance() -> MentraNexSGC {
|
|
274
|
-
if instance
|
|
275
|
-
|
|
326
|
+
if let existing = instance, existing.centralManager != nil {
|
|
327
|
+
return existing
|
|
276
328
|
}
|
|
329
|
+
instance = MentraNexSGC()
|
|
277
330
|
return instance!
|
|
278
331
|
}
|
|
279
332
|
|
|
@@ -295,6 +348,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
295
348
|
private var deviceMaxMTU = 23 // Device's maximum capability
|
|
296
349
|
private var maxChunkSize = 176 // Calculated optimal chunk size
|
|
297
350
|
private var bmpChunkSize = 176 // Image chunk size (iOS-optimized)
|
|
351
|
+
private var protobufSeq: UInt8 = 0 // Rolling sequence for fragmented control messages
|
|
298
352
|
|
|
299
353
|
// MARK: - Command Queue (modeled after ERG1Manager)
|
|
300
354
|
|
|
@@ -333,6 +387,10 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
333
387
|
continuations.append(continuation)
|
|
334
388
|
}
|
|
335
389
|
}
|
|
390
|
+
|
|
391
|
+
func clear() {
|
|
392
|
+
commands.removeAll()
|
|
393
|
+
}
|
|
336
394
|
}
|
|
337
395
|
|
|
338
396
|
private let commandQueue = CommandQueue()
|
|
@@ -360,6 +418,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
360
418
|
Bridge.log(
|
|
361
419
|
"NEX: 💾 Loaded saved device - Name: \(savedDeviceName ?? "None"), Address: \(savedDeviceAddress ?? "None")"
|
|
362
420
|
)
|
|
421
|
+
DeviceStore.shared.apply("glasses", "micEnabled", false)
|
|
363
422
|
}
|
|
364
423
|
|
|
365
424
|
private func setupCommandQueue() {
|
|
@@ -375,7 +434,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
375
434
|
}
|
|
376
435
|
}
|
|
377
436
|
|
|
378
|
-
private func queueChunks(_ chunks: [[UInt8]], waitTimeMs: Int = 0, chunkDelayMs: Int =
|
|
437
|
+
private func queueChunks(_ chunks: [[UInt8]], waitTimeMs: Int = 0, chunkDelayMs: Int = 10) {
|
|
379
438
|
let cmd = BufferedCommand(
|
|
380
439
|
chunks: chunks, waitTimeMs: waitTimeMs, chunkDelayMs: chunkDelayMs
|
|
381
440
|
)
|
|
@@ -384,59 +443,151 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
384
443
|
}
|
|
385
444
|
}
|
|
386
445
|
|
|
387
|
-
|
|
446
|
+
private func waitUntilServicesReady() async {
|
|
447
|
+
if servicesReady {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
await withCheckedContinuation { continuation in
|
|
451
|
+
serviceReadyWaiters.append(continuation)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private func setServicesReady(_ ready: Bool) {
|
|
456
|
+
servicesReady = ready
|
|
457
|
+
guard ready else { return }
|
|
458
|
+
releaseServiceWaiters()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private func releaseServiceWaiters() {
|
|
462
|
+
let waiters = serviceReadyWaiters
|
|
463
|
+
serviceReadyWaiters.removeAll()
|
|
464
|
+
waiters.forEach { $0.resume() }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/// Suspends until CoreBluetooth signals it can accept another write-without-response
|
|
468
|
+
/// (resumed by peripheralIsReady(toSendWriteWithoutResponse:), or by the disconnect
|
|
469
|
+
/// handlers so an in-flight send can't wedge the queue across a drop).
|
|
470
|
+
private func waitUntilReadyToWrite() async {
|
|
471
|
+
await withCheckedContinuation { continuation in
|
|
472
|
+
pendingWriteContinuation = continuation
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private func resumePendingWrite() {
|
|
477
|
+
pendingWriteContinuation?.resume()
|
|
478
|
+
pendingWriteContinuation = nil
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private func emitBleCommandSent(_ packetData: Data) {
|
|
482
|
+
guard packetData.first == PACKET_TYPE_PROTOBUF else { return }
|
|
483
|
+
guard packetData.count > 1 else { return }
|
|
484
|
+
let payload = packetData.subdata(in: 1 ..< packetData.count)
|
|
485
|
+
let commandName: String
|
|
486
|
+
if let phoneToGlasses = try? Mentraos_Ble_PhoneToGlasses(serializedData: payload) {
|
|
487
|
+
commandName = String(describing: phoneToGlasses.payload)
|
|
488
|
+
} else {
|
|
489
|
+
commandName = "UNKNOWN"
|
|
490
|
+
}
|
|
491
|
+
Bridge.sendTypedMessage("send_command_to_ble", body: [
|
|
492
|
+
"command": commandName,
|
|
493
|
+
"commandText": packetData.toHexString(),
|
|
494
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
495
|
+
])
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private func emitBleCommandReceived(_ packetData: Data, payloadDescription: String) {
|
|
499
|
+
Bridge.sendTypedMessage("receive_command_from_ble", body: [
|
|
500
|
+
"command": payloadDescription,
|
|
501
|
+
"commandText": packetData.toHexString(),
|
|
502
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
503
|
+
])
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/// Splits a serialized protobuf message into BLE-sized fragments, each prefixed with a
|
|
507
|
+
/// 4-byte transport header [packetType][seq][totalChunks][chunkIndex] so the firmware can
|
|
508
|
+
/// reassemble messages larger than one MTU. Single-fragment messages set totalChunks = 1
|
|
509
|
+
/// and are decoded directly by the firmware's fast path.
|
|
388
510
|
private func queueDataWithOptimalChunking(
|
|
389
511
|
_ data: Data, packetType: UInt8 = 0x02, waitTimeMs: Int = 0
|
|
390
512
|
) {
|
|
391
|
-
|
|
392
|
-
let effectiveChunkSize = maxChunkSize - 1 // Reserve 1 byte for packet type
|
|
393
|
-
|
|
394
|
-
// Add packet type as first byte
|
|
513
|
+
// Telemetry: report the full logical command (type byte + protobuf) before fragmenting.
|
|
395
514
|
var packetData = Data([packetType])
|
|
396
515
|
packetData.append(data)
|
|
516
|
+
emitBleCommandSent(packetData)
|
|
517
|
+
|
|
518
|
+
let headerSize = 4 // [packetType][seq][totalChunks][chunkIndex]
|
|
519
|
+
let effectiveChunkSize = max(1, maxChunkSize - headerSize)
|
|
520
|
+
let totalChunks = data.isEmpty
|
|
521
|
+
? 1 : Int(ceil(Double(data.count) / Double(effectiveChunkSize)))
|
|
522
|
+
|
|
523
|
+
guard totalChunks <= 255 else {
|
|
524
|
+
Bridge.log(
|
|
525
|
+
"NEX: ❌ Protobuf message too large to fragment (\(totalChunks) chunks) - dropping"
|
|
526
|
+
)
|
|
527
|
+
return
|
|
528
|
+
}
|
|
397
529
|
|
|
398
|
-
|
|
530
|
+
let seq = protobufSeq
|
|
531
|
+
protobufSeq = protobufSeq &+ 1
|
|
532
|
+
|
|
533
|
+
var chunks: [[UInt8]] = []
|
|
399
534
|
var offset = 0
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
let
|
|
403
|
-
|
|
404
|
-
offset
|
|
535
|
+
var index = 0
|
|
536
|
+
while offset < data.count || (index == 0 && data.isEmpty) {
|
|
537
|
+
let end = min(offset + effectiveChunkSize, data.count)
|
|
538
|
+
var frame: [UInt8] = [packetType, seq, UInt8(totalChunks), UInt8(index)]
|
|
539
|
+
if end > offset {
|
|
540
|
+
frame.append(contentsOf: data.subdata(in: offset ..< end))
|
|
541
|
+
}
|
|
542
|
+
chunks.append(frame)
|
|
543
|
+
offset = end
|
|
544
|
+
index += 1
|
|
405
545
|
}
|
|
406
546
|
|
|
407
547
|
Bridge.log(
|
|
408
|
-
"NEX: 📦
|
|
548
|
+
"NEX: 📦 Fragmented protobuf into \(chunks.count) chunk(s) (seq=\(seq), max payload \(effectiveChunkSize) bytes)"
|
|
409
549
|
)
|
|
410
550
|
queueChunks(chunks, waitTimeMs: waitTimeMs)
|
|
411
551
|
}
|
|
412
552
|
|
|
413
|
-
/// Helper method for queueing chunks with optional wait time
|
|
414
|
-
private func queueChunks(_ chunks: [[UInt8]], waitTimeMs: Int = 0) {
|
|
415
|
-
let cmd = BufferedCommand(chunks: chunks, waitTimeMs: waitTimeMs, chunkDelayMs: 8)
|
|
416
|
-
Task { [weak self] in
|
|
417
|
-
await self?.commandQueue.enqueue(cmd)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
553
|
private func processCommand(_ command: BufferedCommand) async {
|
|
422
554
|
guard let peripheral, let writeCharacteristic else {
|
|
423
555
|
Bridge.log("NEX: ⚠️ processCommand: peripheral/characteristic not ready")
|
|
424
556
|
return
|
|
425
557
|
}
|
|
426
558
|
|
|
559
|
+
await waitUntilServicesReady()
|
|
560
|
+
|
|
427
561
|
// Send each chunk sequentially
|
|
428
562
|
for (index, chunk) in command.chunks.enumerated() {
|
|
563
|
+
let timeSinceConnection = Date().timeIntervalSince1970 * 1000 - lastConnectionTimestamp
|
|
564
|
+
if timeSinceConnection < Double(INITIAL_CONNECTION_DELAY_MS) {
|
|
565
|
+
let remainingMs = UInt64(Double(INITIAL_CONNECTION_DELAY_MS) - timeSinceConnection)
|
|
566
|
+
try? await Task.sleep(nanoseconds: remainingMs * 1_000_000)
|
|
567
|
+
}
|
|
429
568
|
let data = Data(chunk)
|
|
430
569
|
Bridge.log(
|
|
431
570
|
"NEX: 📦 Sending chunk \(index) of \(command.chunks.count) to \(peripheral.name ?? "Unknown")"
|
|
432
571
|
)
|
|
433
572
|
Bridge.log("NEX: 📦 Chunk data: \(data.toHexString())")
|
|
434
|
-
|
|
573
|
+
// Write WITHOUT response: a write-with-response is one outstanding request at a
|
|
574
|
+
// time gated on a remote round trip, which puts the (screen-off-throttled) app
|
|
575
|
+
// thread in the path of every fragment and makes captions crawl in the background.
|
|
576
|
+
// .withoutResponse lets CoreBluetooth batch fragments into connection events.
|
|
577
|
+
// CoreBluetooth does NOT call didWriteValueFor for .withoutResponse, so we use its
|
|
578
|
+
// flow-control flag instead of an ack — gating prevents dropped fragments (which
|
|
579
|
+
// would make the firmware discard the whole reassembled caption). See
|
|
580
|
+
// docs/ble-disconnects-during-captions.md in the firmware repo.
|
|
581
|
+
if !peripheral.canSendWriteWithoutResponse {
|
|
582
|
+
await waitUntilReadyToWrite()
|
|
583
|
+
}
|
|
584
|
+
peripheral.writeValue(data, for: writeCharacteristic, type: .withoutResponse)
|
|
435
585
|
|
|
436
586
|
// Delay between chunks except maybe after the last chunk if waitTime will handle it
|
|
437
587
|
if index < command.chunks.count - 1 {
|
|
438
588
|
try? await Task.sleep(nanoseconds: UInt64(command.chunkDelayMs) * 1_000_000)
|
|
439
589
|
}
|
|
590
|
+
try? await Task.sleep(nanoseconds: DELAY_BETWEEN_CHUNKS_SEND_MS * 1_000_000)
|
|
440
591
|
}
|
|
441
592
|
|
|
442
593
|
// Optional wait after the command
|
|
@@ -493,21 +644,14 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
493
644
|
// MARK: - Enhanced Device Filtering (ported from Java)
|
|
494
645
|
|
|
495
646
|
private func isCompatibleNexDevice(_ deviceName: String) -> Bool {
|
|
496
|
-
//
|
|
647
|
+
// Keep in parity with Android MentraNex.kt scan filter.
|
|
497
648
|
let compatiblePrefixes = [
|
|
498
|
-
"
|
|
499
|
-
|
|
500
|
-
// "NEX",
|
|
501
|
-
// "Nex",
|
|
502
|
-
// "MentraNex",
|
|
503
|
-
// "MENTRA_NEX",
|
|
504
|
-
// "Xy_A", // Legacy support
|
|
505
|
-
// "XyBLE_", // Legacy support
|
|
506
|
-
// "MENTRA_LIVE", // Cross-compatibility
|
|
649
|
+
"Nex1-",
|
|
650
|
+
"MENTRA_DISPLAY_",
|
|
507
651
|
]
|
|
508
652
|
|
|
509
653
|
for prefix in compatiblePrefixes {
|
|
510
|
-
if deviceName.
|
|
654
|
+
if deviceName.contains(prefix) {
|
|
511
655
|
Bridge.log("NEX: ✅ Device '\(deviceName)' matches compatible prefix: \(prefix)")
|
|
512
656
|
return true
|
|
513
657
|
}
|
|
@@ -679,6 +823,8 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
679
823
|
|
|
680
824
|
Bridge.log("NEX-CONN: ✅ Bluetooth is powered on, starting scan...")
|
|
681
825
|
_isScanning = true
|
|
826
|
+
connectionState = ConnTypes.SCANNING
|
|
827
|
+
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.SCANNING)
|
|
682
828
|
|
|
683
829
|
// Scan for ALL devices, not just those with specific services
|
|
684
830
|
// Use same options as G1 scanner for consistency
|
|
@@ -725,6 +871,8 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
725
871
|
return "connecting"
|
|
726
872
|
case ConnTypes.CONNECTED:
|
|
727
873
|
return "connected"
|
|
874
|
+
case ConnTypes.SCANNING:
|
|
875
|
+
return "scanning"
|
|
728
876
|
default:
|
|
729
877
|
return "disconnected"
|
|
730
878
|
}
|
|
@@ -755,11 +903,10 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
755
903
|
}
|
|
756
904
|
|
|
757
905
|
@objc func findCompatibleDevices() {
|
|
758
|
-
Bridge.log("NEX-DISCOVERY: Finding compatible devices.
|
|
906
|
+
Bridge.log("NEX-DISCOVERY: Finding compatible devices.")
|
|
759
907
|
|
|
760
|
-
// Clear
|
|
908
|
+
// Clear specific connect target, but keep saved pairing data like Android.
|
|
761
909
|
peripheralToConnectName = nil
|
|
762
|
-
clearSavedDeviceInfo() // This clears UserDefaults and our in-memory cache of saved/preferred devices.
|
|
763
910
|
|
|
764
911
|
Task {
|
|
765
912
|
if centralManager == nil {
|
|
@@ -773,6 +920,9 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
773
920
|
|
|
774
921
|
if centralManager?.state == .poweredOn {
|
|
775
922
|
startScan()
|
|
923
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { [weak self] in
|
|
924
|
+
self?.stopScan()
|
|
925
|
+
}
|
|
776
926
|
} else {
|
|
777
927
|
Bridge.log("NEX-DISCOVERY: Bluetooth not ready, will scan on power on.")
|
|
778
928
|
scanOnPowerOn = true
|
|
@@ -780,16 +930,26 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
780
930
|
}
|
|
781
931
|
}
|
|
782
932
|
|
|
933
|
+
private func sanitizeDisplayText(_ text: String) -> String {
|
|
934
|
+
let normalized = text.replacingOccurrences(of: "—", with: "-")
|
|
935
|
+
let pattern = #"[^A-Za-z0-9 \r\n\.,!\?;:\-\[\]\(\)\{\}'"\+=/]"#
|
|
936
|
+
return normalized.replacingOccurrences(of: pattern, with: "", options: .regularExpression)
|
|
937
|
+
}
|
|
938
|
+
|
|
783
939
|
func sendTextWall(_ text: String) {
|
|
784
940
|
guard nexReady else {
|
|
785
941
|
Bridge.log("NEX: Not ready to display text. Device not initialized.")
|
|
786
942
|
return
|
|
787
943
|
}
|
|
788
944
|
|
|
789
|
-
|
|
945
|
+
// When Chinese captions are disabled (default), strip characters the Nex font
|
|
946
|
+
// can't render (CJK etc.). When enabled, pass the text through unmodified.
|
|
947
|
+
let chineseCaptionsEnabled = DeviceStore.shared.get("bluetooth", "nex_chinese_captions") as? Bool ?? false
|
|
948
|
+
let sanitizedText = chineseCaptionsEnabled ? text : sanitizeDisplayText(text)
|
|
949
|
+
Bridge.log("NEX: Displaying text wall: '\(sanitizedText)'")
|
|
790
950
|
|
|
791
951
|
let displayText = Mentraos_Ble_DisplayText.with {
|
|
792
|
-
$0.text =
|
|
952
|
+
$0.text = sanitizedText
|
|
793
953
|
$0.size = 48
|
|
794
954
|
$0.x = 20
|
|
795
955
|
$0.y = 260
|
|
@@ -809,7 +969,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
809
969
|
}
|
|
810
970
|
|
|
811
971
|
@objc func displayDoubleTextWall(_ textTop: String, textBottom: String) {
|
|
812
|
-
let combinedText = "\(textTop)\n\
|
|
972
|
+
let combinedText = "\(textTop)\n\(textBottom)"
|
|
813
973
|
sendTextWall(combinedText)
|
|
814
974
|
}
|
|
815
975
|
|
|
@@ -944,6 +1104,8 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
944
1104
|
}
|
|
945
1105
|
|
|
946
1106
|
Bridge.log("NEX: Sending \(chunks.count) image chunks")
|
|
1107
|
+
currentImageChunks = chunks
|
|
1108
|
+
isImageSendProgressing = true
|
|
947
1109
|
queueChunks(chunks, waitTimeMs: 50)
|
|
948
1110
|
}
|
|
949
1111
|
|
|
@@ -1006,11 +1168,6 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1006
1168
|
@objc func showHomeScreen() {
|
|
1007
1169
|
Bridge.log("NEX: Showing home screen")
|
|
1008
1170
|
clearDisplay()
|
|
1009
|
-
|
|
1010
|
-
// Send a simple home screen text
|
|
1011
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
1012
|
-
self.sendTextWall("MentraOS Ready")
|
|
1013
|
-
}
|
|
1014
1171
|
}
|
|
1015
1172
|
|
|
1016
1173
|
@objc func exitAllFunctions() {
|
|
@@ -1034,9 +1191,13 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1034
1191
|
return
|
|
1035
1192
|
}
|
|
1036
1193
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1194
|
+
let validBrightness: Int
|
|
1195
|
+
if brightness != -1 {
|
|
1196
|
+
validBrightness = (max(0, min(100, brightness)) * 63) / 100
|
|
1197
|
+
} else {
|
|
1198
|
+
validBrightness = (30 * 63) / 100
|
|
1199
|
+
}
|
|
1200
|
+
Bridge.log("NEX: Setting brightness to wire value \(validBrightness)")
|
|
1040
1201
|
|
|
1041
1202
|
let brightnessConfig = Mentraos_Ble_BrightnessConfig.with {
|
|
1042
1203
|
$0.value = UInt32(validBrightness)
|
|
@@ -1135,7 +1296,33 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1135
1296
|
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1136
1297
|
}
|
|
1137
1298
|
|
|
1299
|
+
func sendVoiceActivityDetectionSetting() {
|
|
1300
|
+
let enabled = DeviceStore.shared.get("bluetooth", "voice_activity_detection_enabled") as? Bool
|
|
1301
|
+
?? BluetoothSdkDefaults.voiceActivityDetectionEnabled
|
|
1302
|
+
Bridge.log("NEX: 🎤 Sending Voice Activity Detection setting to glasses: \(enabled)")
|
|
1303
|
+
|
|
1304
|
+
guard nexReady else {
|
|
1305
|
+
Bridge.log("NEX: Not ready to send VAD setting. Device not initialized.")
|
|
1306
|
+
return
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
let vadConfig = Mentraos_Ble_VadEnabledConfig.with {
|
|
1310
|
+
$0.enabled = enabled
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1314
|
+
$0.vadEnabled = vadConfig
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1318
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1319
|
+
Bridge.sendVoiceActivityDetectionStatus(enabled)
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1138
1322
|
@objc func setMicrophoneEnabled(_ enabled: Bool) {
|
|
1323
|
+
isMicrophoneEnabled = enabled
|
|
1324
|
+
DeviceStore.shared.apply("glasses", "micEnabled", enabled)
|
|
1325
|
+
|
|
1139
1326
|
guard nexReady else {
|
|
1140
1327
|
Bridge.log("NEX: Not ready to set microphone state. Device not initialized.")
|
|
1141
1328
|
return
|
|
@@ -1211,28 +1398,6 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1211
1398
|
|
|
1212
1399
|
// MARK: - Utility Methods
|
|
1213
1400
|
|
|
1214
|
-
@objc func sendPongResponse() {
|
|
1215
|
-
guard nexReady else {
|
|
1216
|
-
Bridge.log("NEX: Not ready to send pong. Device not initialized.")
|
|
1217
|
-
return
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
let timestamp = Date().timeIntervalSince1970 * 1000
|
|
1221
|
-
Bridge.log("NEX: Sending pong response (Time: \(timestamp))")
|
|
1222
|
-
|
|
1223
|
-
let pongResponse = Mentraos_Ble_PongResponse()
|
|
1224
|
-
|
|
1225
|
-
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1226
|
-
$0.pong = pongResponse
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
let protobufData = try! phoneToGlasses.serializedData()
|
|
1230
|
-
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1231
|
-
|
|
1232
|
-
// Notify about heartbeat sent (pong response)
|
|
1233
|
-
notifyHeartbeatSent(timestamp)
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
1401
|
@objc func isDeviceReady() -> Bool {
|
|
1237
1402
|
nexReady && connectionState == ConnTypes.CONNECTED
|
|
1238
1403
|
}
|
|
@@ -1321,8 +1486,6 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1321
1486
|
handleDeviceInfoJson(json)
|
|
1322
1487
|
case "button_event":
|
|
1323
1488
|
handleButtonEventJson(json)
|
|
1324
|
-
case "ping":
|
|
1325
|
-
handlePingJson(json)
|
|
1326
1489
|
case "vad_event":
|
|
1327
1490
|
handleVadEventJson(json)
|
|
1328
1491
|
case "imu_data":
|
|
@@ -1342,6 +1505,9 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1342
1505
|
let glassesToPhone = try Mentraos_Ble_GlassesToPhone(serializedData: protobufData)
|
|
1343
1506
|
Bridge.log("NEX: Processing protobuf payload case: \(glassesToPhone.payload)")
|
|
1344
1507
|
|
|
1508
|
+
let fullPacket = Data([PACKET_TYPE_PROTOBUF]) + protobufData
|
|
1509
|
+
emitBleCommandReceived(fullPacket, payloadDescription: String(describing: glassesToPhone.payload))
|
|
1510
|
+
|
|
1345
1511
|
switch glassesToPhone.payload {
|
|
1346
1512
|
case let .batteryStatus(batteryStatus):
|
|
1347
1513
|
handleBatteryStatusProtobuf(batteryStatus)
|
|
@@ -1358,9 +1524,6 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1358
1524
|
case let .headUpAngleSet(headUpAngleResponse):
|
|
1359
1525
|
handleHeadUpAngleResponseProtobuf(headUpAngleResponse)
|
|
1360
1526
|
|
|
1361
|
-
case let .ping(pingRequest):
|
|
1362
|
-
handlePingProtobuf(pingRequest)
|
|
1363
|
-
|
|
1364
1527
|
case let .vadEvent(vadEvent):
|
|
1365
1528
|
handleVadEventProtobuf(vadEvent)
|
|
1366
1529
|
|
|
@@ -1395,6 +1558,11 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1395
1558
|
"NEX: Received audio data - sequence: \(sequenceNumber), size: \(audioData.count) bytes"
|
|
1396
1559
|
)
|
|
1397
1560
|
|
|
1561
|
+
if lastReceivedLc3Sequence != -1, UInt8((lastReceivedLc3Sequence + 1) & 0xFF) != sequenceNumber {
|
|
1562
|
+
Bridge.log("NEX: LC3 packet sequence mismatch. Expected \((lastReceivedLc3Sequence + 1) & 0xFF), got \(sequenceNumber)")
|
|
1563
|
+
}
|
|
1564
|
+
lastReceivedLc3Sequence = Int(sequenceNumber)
|
|
1565
|
+
|
|
1398
1566
|
// Update @Published property (G1-compatible approach)
|
|
1399
1567
|
// Create packet with sequence number prefix like G1 expects
|
|
1400
1568
|
var packetData = Data()
|
|
@@ -1402,6 +1570,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1402
1570
|
packetData.append(audioData)
|
|
1403
1571
|
|
|
1404
1572
|
compressedVoiceData = packetData
|
|
1573
|
+
DeviceManager.shared.handleGlassesMicData(audioData, 40)
|
|
1405
1574
|
}
|
|
1406
1575
|
|
|
1407
1576
|
private func processImageData(_ imageData: Data) {
|
|
@@ -1463,33 +1632,6 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1463
1632
|
emitEvent("HeadUpAngleResponseEvent", body: eventBody)
|
|
1464
1633
|
}
|
|
1465
1634
|
|
|
1466
|
-
private func handlePingProtobuf(_: Mentraos_Ble_PingRequest) {
|
|
1467
|
-
let timestamp = Date().timeIntervalSince1970 * 1000
|
|
1468
|
-
|
|
1469
|
-
Bridge.log("NEX: 💓 Received PING from glasses (Time: \(timestamp))")
|
|
1470
|
-
|
|
1471
|
-
// Automatically send pong response
|
|
1472
|
-
sendPongResponse()
|
|
1473
|
-
|
|
1474
|
-
// Emit heartbeat received event
|
|
1475
|
-
let eventBody: [String: Any] = [
|
|
1476
|
-
"heartbeat_received": [
|
|
1477
|
-
"timestamp": timestamp,
|
|
1478
|
-
"device_model": "Mentra Display",
|
|
1479
|
-
],
|
|
1480
|
-
]
|
|
1481
|
-
|
|
1482
|
-
emitEvent("HeartbeatReceivedEvent", body: eventBody)
|
|
1483
|
-
|
|
1484
|
-
// Query battery status periodically (every 10 pings like Java implementation)
|
|
1485
|
-
heartbeatCount += 1
|
|
1486
|
-
if heartbeatCount % 10 == 0 {
|
|
1487
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
1488
|
-
self.queryBatteryStatus()
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
1635
|
private func handleVadEventProtobuf(_ vadEvent: Mentraos_Ble_VadEvent) {
|
|
1494
1636
|
let vadActiveState = vadEvent.state == .active
|
|
1495
1637
|
|
|
@@ -1511,11 +1653,12 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1511
1653
|
switch status {
|
|
1512
1654
|
case .ok:
|
|
1513
1655
|
Bridge.log("NEX: Image transfer completed successfully")
|
|
1514
|
-
|
|
1656
|
+
currentImageChunks.removeAll()
|
|
1657
|
+
isImageSendProgressing = false
|
|
1515
1658
|
|
|
1516
1659
|
case .incomplete:
|
|
1517
1660
|
Bridge.log("NEX: Image transfer incomplete - Missing chunks: \(missingChunks)")
|
|
1518
|
-
|
|
1661
|
+
resendImageMissingChunks(missingChunks)
|
|
1519
1662
|
|
|
1520
1663
|
default:
|
|
1521
1664
|
Bridge.log("NEX: Unknown image transfer status")
|
|
@@ -1533,6 +1676,21 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1533
1676
|
emitEvent("ImageTransferCompleteEvent", body: eventBody)
|
|
1534
1677
|
}
|
|
1535
1678
|
|
|
1679
|
+
private func resendImageMissingChunks(_ missingChunks: [UInt32]) {
|
|
1680
|
+
guard isImageSendProgressing, !currentImageChunks.isEmpty, !missingChunks.isEmpty else {
|
|
1681
|
+
return
|
|
1682
|
+
}
|
|
1683
|
+
let retransmit = missingChunks.compactMap { index -> [UInt8]? in
|
|
1684
|
+
let i = Int(index)
|
|
1685
|
+
guard i >= 0, i < currentImageChunks.count else { return nil }
|
|
1686
|
+
return currentImageChunks[i]
|
|
1687
|
+
}
|
|
1688
|
+
if !retransmit.isEmpty {
|
|
1689
|
+
Bridge.log("NEX: Resending \(retransmit.count) missing image chunks")
|
|
1690
|
+
queueChunks(retransmit)
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1536
1694
|
private func handleImuDataProtobuf(_ imuData: Mentraos_Ble_ImuData) {
|
|
1537
1695
|
Bridge.log("NEX: 📊 IMU Data: \(imuData)")
|
|
1538
1696
|
|
|
@@ -1579,7 +1737,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1579
1737
|
let level = json["level"] as? Int ?? -1
|
|
1580
1738
|
let isCharging = json["charging"] as? Bool ?? false
|
|
1581
1739
|
|
|
1582
|
-
Bridge.log("NEX: 🔋 JSON Battery Status - Level: \(level)%, Charging: \(
|
|
1740
|
+
Bridge.log("NEX: 🔋 JSON Battery Status - Level: \(level)%, Charging: \(isCharging)")
|
|
1583
1741
|
|
|
1584
1742
|
// Update @Published properties (G1-compatible approach)
|
|
1585
1743
|
DeviceStore.shared.apply("glasses", "batteryLevel", level)
|
|
@@ -1615,25 +1773,6 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1615
1773
|
emitEvent("ButtonPressEvent", body: eventBody)
|
|
1616
1774
|
}
|
|
1617
1775
|
|
|
1618
|
-
private func handlePingJson(_: [String: Any]) {
|
|
1619
|
-
let timestamp = Date().timeIntervalSince1970 * 1000
|
|
1620
|
-
|
|
1621
|
-
Bridge.log("NEX: 💓 JSON PING received (Time: \(timestamp))")
|
|
1622
|
-
|
|
1623
|
-
// Send pong response
|
|
1624
|
-
sendPongResponse()
|
|
1625
|
-
|
|
1626
|
-
// Emit heartbeat received event
|
|
1627
|
-
let eventBody: [String: Any] = [
|
|
1628
|
-
"heartbeat_received": [
|
|
1629
|
-
"timestamp": timestamp,
|
|
1630
|
-
"device_model": "Mentra Display",
|
|
1631
|
-
],
|
|
1632
|
-
]
|
|
1633
|
-
|
|
1634
|
-
emitEvent("HeartbeatReceivedEvent", body: eventBody)
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
1776
|
private func handleVadEventJson(_ json: [String: Any]) {
|
|
1638
1777
|
let vadActiveState = json["vad"] as? Bool ?? false
|
|
1639
1778
|
|
|
@@ -1682,28 +1821,16 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1682
1821
|
|
|
1683
1822
|
private func notifyHeartbeatSent(_ timestamp: TimeInterval) {
|
|
1684
1823
|
lastHeartbeatSentTime = timestamp
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
"timestamp": timestamp,
|
|
1689
|
-
"device_model": "Mentra Display",
|
|
1690
|
-
],
|
|
1691
|
-
]
|
|
1692
|
-
|
|
1693
|
-
emitEvent("HeartbeatSentEvent", body: eventBody)
|
|
1824
|
+
Bridge.sendTypedMessage("heartbeat_sent", body: [
|
|
1825
|
+
"timestamp": timestamp,
|
|
1826
|
+
])
|
|
1694
1827
|
}
|
|
1695
1828
|
|
|
1696
1829
|
private func notifyHeartbeatReceived(_ timestamp: TimeInterval) {
|
|
1697
1830
|
lastHeartbeatReceivedTime = timestamp
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
"timestamp": timestamp,
|
|
1702
|
-
"device_model": "Mentra Display",
|
|
1703
|
-
],
|
|
1704
|
-
]
|
|
1705
|
-
|
|
1706
|
-
emitEvent("HeartbeatReceivedEvent", body: eventBody)
|
|
1831
|
+
Bridge.sendTypedMessage("heartbeat_received", body: [
|
|
1832
|
+
"timestamp": timestamp,
|
|
1833
|
+
])
|
|
1707
1834
|
}
|
|
1708
1835
|
|
|
1709
1836
|
@objc func getLastHeartbeatSentTime() -> TimeInterval {
|
|
@@ -1723,8 +1850,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1723
1850
|
stopMicBeat()
|
|
1724
1851
|
}
|
|
1725
1852
|
|
|
1726
|
-
|
|
1727
|
-
setMicrophoneEnabled(true)
|
|
1853
|
+
sendSetMicEnabled(true, delaySeconds: 0.01)
|
|
1728
1854
|
micBeatCount += 1
|
|
1729
1855
|
|
|
1730
1856
|
// Schedule periodic mic beat (like Java lines 1753-1762)
|
|
@@ -1732,18 +1858,24 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1732
1858
|
[weak self] _ in
|
|
1733
1859
|
guard let self else { return }
|
|
1734
1860
|
Bridge.log("NEX: SENDING MIC BEAT")
|
|
1735
|
-
self.
|
|
1861
|
+
self.sendSetMicEnabled(self.shouldUseGlassesMic, delaySeconds: 0.001)
|
|
1736
1862
|
}
|
|
1737
1863
|
}
|
|
1738
1864
|
|
|
1739
1865
|
private func stopMicBeat() {
|
|
1740
|
-
|
|
1866
|
+
sendSetMicEnabled(false, delaySeconds: 0.01)
|
|
1741
1867
|
micBeatTimer?.invalidate()
|
|
1742
1868
|
micBeatTimer = nil
|
|
1743
1869
|
micBeatCount = 0
|
|
1744
1870
|
Bridge.log("NEX: Stopped mic beat")
|
|
1745
1871
|
}
|
|
1746
1872
|
|
|
1873
|
+
private func sendSetMicEnabled(_ enabled: Bool, delaySeconds: TimeInterval) {
|
|
1874
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { [weak self] in
|
|
1875
|
+
self?.setMicrophoneEnabled(enabled)
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1747
1879
|
private func sendWhiteListCommand() {
|
|
1748
1880
|
guard !whiteListedAlready else {
|
|
1749
1881
|
Bridge.log("NEX: Whitelist already sent, skipping")
|
|
@@ -1842,13 +1974,45 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1842
1974
|
|
|
1843
1975
|
/// Save microphone state before disconnection (like Java implementation)
|
|
1844
1976
|
private func saveMicrophoneStateBeforeDisconnection() {
|
|
1845
|
-
UserDefaults.standard.set(
|
|
1846
|
-
microphoneStateBeforeDisconnection =
|
|
1847
|
-
Bridge.log("NEX: Saved microphone state before disconnection: \(
|
|
1977
|
+
UserDefaults.standard.set(isMicrophoneEnabled, forKey: "microphoneStateBeforeDisconnection")
|
|
1978
|
+
microphoneStateBeforeDisconnection = isMicrophoneEnabled
|
|
1979
|
+
Bridge.log("NEX: Saved microphone state before disconnection: \(isMicrophoneEnabled)")
|
|
1848
1980
|
}
|
|
1849
1981
|
|
|
1850
1982
|
@objc func disconnect() {
|
|
1851
1983
|
Bridge.log("NEX: 🔌 User-initiated disconnect")
|
|
1984
|
+
// Light teardown: drop the link but stay able to reconnect.
|
|
1985
|
+
sendIntentionalDisconnectThen { [weak self] in self?.finalizeDisconnect() }
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/// Best-effort: tell the glasses this disconnect is intentional so they return to
|
|
1989
|
+
/// the welcome screen immediately rather than holding the last frame through the
|
|
1990
|
+
/// firmware's unexpected-disconnect grace period, then run `teardown` after a short
|
|
1991
|
+
/// window to let the write flush. Falls straight through if nothing is connected.
|
|
1992
|
+
private func sendIntentionalDisconnectThen(_ teardown: @escaping () -> Void) {
|
|
1993
|
+
isDisconnecting = true
|
|
1994
|
+
stopReconnectionTimer()
|
|
1995
|
+
guard peripheral != nil, servicesReady else {
|
|
1996
|
+
teardown()
|
|
1997
|
+
return
|
|
1998
|
+
}
|
|
1999
|
+
sendDisconnectRequest()
|
|
2000
|
+
MentraNexSGC._bluetoothQueue.asyncAfter(deadline: .now() + 0.25, execute: teardown)
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
private func sendDisconnectRequest() {
|
|
2004
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
2005
|
+
$0.disconnect = Mentraos_Ble_DisconnectRequest()
|
|
2006
|
+
}
|
|
2007
|
+
guard let protobufData = try? phoneToGlasses.serializedData() else {
|
|
2008
|
+
Bridge.log("NEX: ⚠️ Failed to serialize DisconnectRequest")
|
|
2009
|
+
return
|
|
2010
|
+
}
|
|
2011
|
+
Bridge.log("NEX: 📤 Sending DisconnectRequest before teardown")
|
|
2012
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
private func finalizeDisconnect() {
|
|
1852
2016
|
if let peripheral {
|
|
1853
2017
|
// Save microphone state before disconnection (like Java implementation)
|
|
1854
2018
|
saveMicrophoneStateBeforeDisconnection()
|
|
@@ -1856,16 +2020,25 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1856
2020
|
// Stop mic beat system
|
|
1857
2021
|
stopMicBeat()
|
|
1858
2022
|
|
|
1859
|
-
isDisconnecting = true
|
|
1860
2023
|
connectionState = ConnTypes.DISCONNECTED
|
|
1861
2024
|
centralManager?.cancelPeripheralConnection(peripheral)
|
|
1862
2025
|
}
|
|
2026
|
+
setServicesReady(false)
|
|
2027
|
+
releaseServiceWaiters()
|
|
2028
|
+
Task { await commandQueue.clear() }
|
|
2029
|
+
resumePendingWrite()
|
|
1863
2030
|
stopReconnectionTimer()
|
|
1864
2031
|
}
|
|
1865
2032
|
|
|
1866
2033
|
// MARK: - Lifecycle Management (ported from Java)
|
|
1867
2034
|
|
|
1868
2035
|
@objc func destroy() {
|
|
2036
|
+
// Route through the shared path so forget()/cleanup() also signal an
|
|
2037
|
+
// intentional disconnect to the glasses before the link goes down.
|
|
2038
|
+
sendIntentionalDisconnectThen { [weak self] in self?.performDestroy() }
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
private func performDestroy() {
|
|
1869
2042
|
Bridge.log("NEX: 💥 Destroying MentraNexSGC instance")
|
|
1870
2043
|
|
|
1871
2044
|
isKilled = true
|
|
@@ -1899,11 +2072,22 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1899
2072
|
|
|
1900
2073
|
// Clear discovery cache
|
|
1901
2074
|
discoveredPeripherals.removeAll()
|
|
2075
|
+
setServicesReady(false)
|
|
2076
|
+
releaseServiceWaiters()
|
|
2077
|
+
Task { await commandQueue.clear() }
|
|
2078
|
+
resumePendingWrite()
|
|
1902
2079
|
|
|
1903
2080
|
Bridge.log("NEX: ✅ MentraNexSGC destroyed successfully")
|
|
1904
2081
|
// Reset initialization flags
|
|
1905
2082
|
whiteListedAlready = false
|
|
1906
2083
|
protobufVersionPosted = false
|
|
2084
|
+
currentImageChunks.removeAll()
|
|
2085
|
+
isImageSendProgressing = false
|
|
2086
|
+
currentMTU = MTU_DEFAULT
|
|
2087
|
+
deviceMaxMTU = MTU_DEFAULT
|
|
2088
|
+
maxChunkSize = MTU_DEFAULT - 10
|
|
2089
|
+
bmpChunkSize = MTU_DEFAULT - 20
|
|
2090
|
+
updateConnectedState(isConnected: false)
|
|
1907
2091
|
}
|
|
1908
2092
|
|
|
1909
2093
|
@objc func reset() {
|
|
@@ -1933,6 +2117,9 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1933
2117
|
micBeatCount = 0
|
|
1934
2118
|
shouldUseGlassesMic = true
|
|
1935
2119
|
microphoneStateBeforeDisconnection = false
|
|
2120
|
+
currentImageChunks.removeAll()
|
|
2121
|
+
isImageSendProgressing = false
|
|
2122
|
+
updateConnectedState(isConnected: false)
|
|
1936
2123
|
}
|
|
1937
2124
|
|
|
1938
2125
|
// MARK: - Helper Methods (like G1)
|
|
@@ -1949,6 +2136,13 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
1949
2136
|
Bridge.sendDiscoveredDevice(DeviceTypes.NEX, name)
|
|
1950
2137
|
}
|
|
1951
2138
|
|
|
2139
|
+
private func updateConnectedState(isConnected: Bool) {
|
|
2140
|
+
connectionState = isConnected ? ConnTypes.CONNECTED : ConnTypes.DISCONNECTED
|
|
2141
|
+
DeviceStore.shared.apply("glasses", "connected", isConnected)
|
|
2142
|
+
DeviceStore.shared.apply("glasses", "fullyBooted", isConnected)
|
|
2143
|
+
DeviceStore.shared.apply("glasses", "connectionState", isConnected ? ConnTypes.CONNECTED : ConnTypes.DISCONNECTED)
|
|
2144
|
+
}
|
|
2145
|
+
|
|
1952
2146
|
@objc func checkBluetoothState() {
|
|
1953
2147
|
Bridge.log("NEX: 🔍 Checking Bluetooth State...")
|
|
1954
2148
|
if let centralManager {
|
|
@@ -2025,10 +2219,9 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2025
2219
|
return
|
|
2026
2220
|
}
|
|
2027
2221
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
// }
|
|
2222
|
+
guard isCompatibleNexDevice(deviceName) else {
|
|
2223
|
+
return
|
|
2224
|
+
}
|
|
2032
2225
|
|
|
2033
2226
|
Bridge.log("NEX-CONN: 🎯 === Compatible Nex Device Found ===")
|
|
2034
2227
|
Bridge.log("NEX-CONN: 📱 Device Name: \(deviceName)")
|
|
@@ -2090,6 +2283,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2090
2283
|
self.peripheral = peripheral
|
|
2091
2284
|
isConnecting = true
|
|
2092
2285
|
connectionState = ConnTypes.CONNECTING
|
|
2286
|
+
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.CONNECTING)
|
|
2093
2287
|
|
|
2094
2288
|
// Use connection options for better reliability (from Java implementation)
|
|
2095
2289
|
let connectionOptions: [String: Any] = [
|
|
@@ -2124,6 +2318,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2124
2318
|
Bridge.log("NEX-CONN: 💾 Device information saved for reliable reconnection")
|
|
2125
2319
|
peripheral.delegate = self
|
|
2126
2320
|
Bridge.log("NEX-CONN: 🔍 Discovering services...")
|
|
2321
|
+
setServicesReady(false)
|
|
2127
2322
|
peripheral.discoverServices([MAIN_SERVICE_UUID])
|
|
2128
2323
|
|
|
2129
2324
|
// Reset any failed connection attempt counters
|
|
@@ -2139,6 +2334,10 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2139
2334
|
)
|
|
2140
2335
|
isConnecting = false
|
|
2141
2336
|
connectionState = ConnTypes.DISCONNECTED
|
|
2337
|
+
setServicesReady(false)
|
|
2338
|
+
releaseServiceWaiters()
|
|
2339
|
+
Task { await commandQueue.clear() }
|
|
2340
|
+
resumePendingWrite()
|
|
2142
2341
|
self.peripheral = nil // Reset peripheral on failure to allow reconnection
|
|
2143
2342
|
// Optionally, start reconnection attempts here
|
|
2144
2343
|
if !isDisconnecting, !isKilled {
|
|
@@ -2161,6 +2360,10 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2161
2360
|
// Reset connection state
|
|
2162
2361
|
// Save microphone state before disconnection (like Java implementation)
|
|
2163
2362
|
saveMicrophoneStateBeforeDisconnection()
|
|
2363
|
+
setServicesReady(false)
|
|
2364
|
+
releaseServiceWaiters()
|
|
2365
|
+
Task { await commandQueue.clear() }
|
|
2366
|
+
resumePendingWrite()
|
|
2164
2367
|
|
|
2165
2368
|
// Reset protobuf version posted flag for next connection (like Java implementation)
|
|
2166
2369
|
protobufVersionPosted = false
|
|
@@ -2191,6 +2394,13 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2191
2394
|
writeCharacteristic = nil
|
|
2192
2395
|
notifyCharacteristic = nil
|
|
2193
2396
|
connectionState = ConnTypes.DISCONNECTED
|
|
2397
|
+
currentMTU = MTU_DEFAULT
|
|
2398
|
+
deviceMaxMTU = MTU_DEFAULT
|
|
2399
|
+
maxChunkSize = MTU_DEFAULT - 10
|
|
2400
|
+
bmpChunkSize = MTU_DEFAULT - 20
|
|
2401
|
+
currentImageChunks.removeAll()
|
|
2402
|
+
isImageSendProgressing = false
|
|
2403
|
+
updateConnectedState(isConnected: false)
|
|
2194
2404
|
|
|
2195
2405
|
// Clear command queue if needed
|
|
2196
2406
|
if isQueueWorkerRunning {
|
|
@@ -2228,8 +2438,10 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2228
2438
|
// iOS MTU is automatically negotiated - we can only discover the current value
|
|
2229
2439
|
// No manual MTU request available on iOS (platform limitation)
|
|
2230
2440
|
|
|
2231
|
-
// Get current MTU capability (iOS-specific approach)
|
|
2232
|
-
|
|
2441
|
+
// Get current MTU capability (iOS-specific approach). Query for .withoutResponse
|
|
2442
|
+
// since that's the write type the caption path uses; its limit can differ from
|
|
2443
|
+
// .withResponse, and sizing chunks to it avoids oversized writes being dropped.
|
|
2444
|
+
let maxWriteLength = peripheral.maximumWriteValueLength(for: .withoutResponse)
|
|
2233
2445
|
let actualMTU = maxWriteLength + 3 // Add L2CAP header size
|
|
2234
2446
|
|
|
2235
2447
|
Bridge.log("NEX: 📊 iOS MTU Discovery Results:")
|
|
@@ -2265,7 +2477,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2265
2477
|
|
|
2266
2478
|
// Calculate optimal chunk sizes based on iOS MTU constraints
|
|
2267
2479
|
maxChunkSize = currentMTU - 10 // Reserve 10 bytes for headers
|
|
2268
|
-
bmpChunkSize = currentMTU -
|
|
2480
|
+
bmpChunkSize = currentMTU - 20
|
|
2269
2481
|
|
|
2270
2482
|
Bridge.log("NEX: 📦 Optimized Chunk Sizes:")
|
|
2271
2483
|
Bridge.log("NEX: 📄 Data Chunk Size: \(maxChunkSize) bytes")
|
|
@@ -2289,6 +2501,8 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2289
2501
|
Bridge.log("NEX-CONN: ✅ Device initialization complete - ready for communication")
|
|
2290
2502
|
nexReady = true
|
|
2291
2503
|
connectionState = ConnTypes.CONNECTED
|
|
2504
|
+
lastConnectionTimestamp = Date().timeIntervalSince1970 * 1000
|
|
2505
|
+
updateConnectedState(isConnected: true)
|
|
2292
2506
|
|
|
2293
2507
|
// Update @Published property for device ready state
|
|
2294
2508
|
deviceReady = true
|
|
@@ -2342,9 +2556,14 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2342
2556
|
self.postProtobufSchemaVersionInfo()
|
|
2343
2557
|
}
|
|
2344
2558
|
|
|
2345
|
-
// 6.
|
|
2346
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
2347
|
-
|
|
2559
|
+
// 6. Version request is removed in current schema; Android sends no-op.
|
|
2560
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
2561
|
+
Bridge.log("NEX: Skipping version request; schema removed VersionRequest")
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// 7. Push current glasses-side Voice Activity Detection setting
|
|
2565
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
2566
|
+
self.sendVoiceActivityDetectionSetting()
|
|
2348
2567
|
}
|
|
2349
2568
|
|
|
2350
2569
|
Bridge.log("NEX-CONN: ✅ Java-compatible initialization sequence started")
|
|
@@ -2449,10 +2668,15 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2449
2668
|
Bridge.log(
|
|
2450
2669
|
"NEX-CONN: ❌ Error writing value to \(characteristic.uuid): \(error.localizedDescription)"
|
|
2451
2670
|
)
|
|
2671
|
+
resumePendingWrite()
|
|
2452
2672
|
return
|
|
2453
2673
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2674
|
+
resumePendingWrite()
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
func peripheralIsReady(toSendWriteWithoutResponse _: CBPeripheral) {
|
|
2678
|
+
// CoreBluetooth can accept more write-without-response data; unblock the sender.
|
|
2679
|
+
resumePendingWrite()
|
|
2456
2680
|
}
|
|
2457
2681
|
|
|
2458
2682
|
func peripheral(
|
|
@@ -2470,10 +2694,16 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
|
|
|
2470
2694
|
Bridge.log(
|
|
2471
2695
|
"NEX-CONN: ✅ Successfully subscribed to notifications for characteristic \(characteristic.uuid.uuidString)."
|
|
2472
2696
|
)
|
|
2697
|
+
if characteristic.uuid == NOTIFY_CHAR_UUID {
|
|
2698
|
+
setServicesReady(true)
|
|
2699
|
+
}
|
|
2473
2700
|
} else {
|
|
2474
2701
|
Bridge.log(
|
|
2475
2702
|
"NEX-CONN: unsubscribed from notifications for characteristic \(characteristic.uuid.uuidString)."
|
|
2476
2703
|
)
|
|
2704
|
+
if characteristic.uuid == NOTIFY_CHAR_UUID {
|
|
2705
|
+
setServicesReady(false)
|
|
2706
|
+
}
|
|
2477
2707
|
}
|
|
2478
2708
|
}
|
|
2479
2709
|
}
|