@mentra/bluetooth-sdk 0.1.16 → 0.1.18
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/Bridge.kt +52 -4
- package/ios/LocalPhotoUploadServer.swift +78 -0
- package/ios/MentraPhotoReceiverModule.swift +10 -0
- package/ios/Source/BluetoothSdkDefaults.swift +2 -30
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/internal/BleTraceLogger.swift +192 -0
- package/ios/Source/sgcs/MentraLive.swift +299 -9
- package/package.json +4 -1
- package/scripts/inject-ios-sdk-version.mjs +65 -0
package/README.md
CHANGED
|
@@ -475,7 +475,7 @@ For bare native iOS apps, use the public SwiftPM repository:
|
|
|
475
475
|
https://github.com/Mentra-Community/mentra-bluetooth-sdk-ios.git
|
|
476
476
|
```
|
|
477
477
|
|
|
478
|
-
Select version `0.1.
|
|
478
|
+
Select version `0.1.18`, then add the `MentraBluetoothSDK` product to your app target.
|
|
479
479
|
|
|
480
480
|
For local SDK development, add this package folder directly in Xcode:
|
|
481
481
|
|
|
@@ -30,6 +30,18 @@ public class Bridge private constructor() {
|
|
|
30
30
|
private const val MIC_CHANNELS = 1
|
|
31
31
|
private const val LC3_FRAME_DURATION_MS = 10
|
|
32
32
|
private const val DEFAULT_LC3_FRAME_SIZE_BYTES = 60
|
|
33
|
+
private val AUDIO_TRACE_METADATA_KEYS =
|
|
34
|
+
listOf(
|
|
35
|
+
"sampleRate",
|
|
36
|
+
"bitsPerSample",
|
|
37
|
+
"channels",
|
|
38
|
+
"encoding",
|
|
39
|
+
"frameDurationMs",
|
|
40
|
+
"frameSizeBytes",
|
|
41
|
+
"bitrate",
|
|
42
|
+
"packetizedFromGlasses",
|
|
43
|
+
"voiceActivityDetectionEnabled",
|
|
44
|
+
)
|
|
33
45
|
|
|
34
46
|
@Volatile private var instance: Bridge? = null
|
|
35
47
|
|
|
@@ -731,13 +743,14 @@ public class Bridge private constructor() {
|
|
|
731
743
|
return
|
|
732
744
|
}
|
|
733
745
|
|
|
734
|
-
|
|
746
|
+
val tracePayload = tracePayloadForTypedMessage(type, mutableBody as Map<String, Any>)
|
|
747
|
+
if (tracePayload != null) {
|
|
735
748
|
try {
|
|
736
749
|
BleTraceLogger.logMap(
|
|
737
750
|
"phone_to_app",
|
|
738
751
|
"sdk_event_dispatch",
|
|
739
752
|
type,
|
|
740
|
-
|
|
753
|
+
tracePayload,
|
|
741
754
|
)
|
|
742
755
|
} catch (e: Exception) {
|
|
743
756
|
Log.d(TAG, "BLE trace logging failed for typed message '$type'", e)
|
|
@@ -762,8 +775,43 @@ public class Bridge private constructor() {
|
|
|
762
775
|
}
|
|
763
776
|
}
|
|
764
777
|
|
|
765
|
-
private fun
|
|
766
|
-
type
|
|
778
|
+
private fun tracePayloadForTypedMessage(
|
|
779
|
+
type: String,
|
|
780
|
+
body: Map<String, Any>
|
|
781
|
+
): Map<String, Any>? =
|
|
782
|
+
when {
|
|
783
|
+
type == "log" -> null
|
|
784
|
+
isAudioPayloadEvent(type) -> audioTracePayload(type, body)
|
|
785
|
+
else -> body
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private fun isAudioPayloadEvent(type: String): Boolean =
|
|
789
|
+
type == "mic_pcm" || type == "mic_lc3"
|
|
790
|
+
|
|
791
|
+
private fun audioTracePayload(type: String, body: Map<String, Any>): Map<String, Any> {
|
|
792
|
+
val payload = HashMap<String, Any>()
|
|
793
|
+
payload["type"] = type
|
|
794
|
+
payload["timestamp"] = System.currentTimeMillis()
|
|
795
|
+
payload["payloadOmitted"] = true
|
|
796
|
+
payload["payloadOmittedReason"] = "audio"
|
|
797
|
+
|
|
798
|
+
val audioBytes =
|
|
799
|
+
when (type) {
|
|
800
|
+
"mic_pcm" -> (body["pcm"] as? ByteArray)?.size
|
|
801
|
+
"mic_lc3" -> (body["lc3"] as? ByteArray)?.size
|
|
802
|
+
else -> null
|
|
803
|
+
}
|
|
804
|
+
audioBytes?.let { payload["audioBytes"] = it }
|
|
805
|
+
|
|
806
|
+
AUDIO_TRACE_METADATA_KEYS.forEach { key ->
|
|
807
|
+
val value = body[key]
|
|
808
|
+
if (value != null) {
|
|
809
|
+
payload[key] = value
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return payload
|
|
814
|
+
}
|
|
767
815
|
}
|
|
768
816
|
|
|
769
817
|
init {
|
|
@@ -104,6 +104,7 @@ final class LocalPhotoUploadServer {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
private func handle(_ connection: NWConnection) {
|
|
107
|
+
traceWifiInput("photo_receiver_connection", connection: connection, state: nil)
|
|
107
108
|
connection.start(queue: queue)
|
|
108
109
|
receive(connection, state: RequestReadState())
|
|
109
110
|
}
|
|
@@ -171,6 +172,14 @@ final class LocalPhotoUploadServer {
|
|
|
171
172
|
let request = HttpRequest.parse(headerText)
|
|
172
173
|
state.request = request
|
|
173
174
|
onLog("\(request.method) \(request.path)")
|
|
175
|
+
var requestTraceValues: [String: Any] = [
|
|
176
|
+
"method": request.method,
|
|
177
|
+
"path": request.path,
|
|
178
|
+
]
|
|
179
|
+
if let contentLength = request.headers["content-length"].flatMap(Int.init) {
|
|
180
|
+
requestTraceValues["contentLength"] = contentLength
|
|
181
|
+
}
|
|
182
|
+
traceWifiInput("photo_receiver_request", connection: connection, state: state, values: requestTraceValues)
|
|
174
183
|
|
|
175
184
|
if request.method == "GET", request.path == "/" || request.path == "/health" {
|
|
176
185
|
writeJson(connection, status: 200, body: #"{"ok":true,"service":"mentra-photo-upload-receiver"}"#)
|
|
@@ -263,6 +272,29 @@ final class LocalPhotoUploadServer {
|
|
|
263
272
|
)
|
|
264
273
|
|
|
265
274
|
onLog("upload fields=\(parsed.fields.keys.joined(separator: ",")) requestId=\(requestId ?? "") bytes=\(photoPart.byteCount) saved=\(photoFile.path)")
|
|
275
|
+
var uploadReceivedTraceValues: [String: Any] = [
|
|
276
|
+
"durationMs": Int(Date().timeIntervalSince1970 * 1000 - state.startMs),
|
|
277
|
+
"photoBytes": photoPart.byteCount,
|
|
278
|
+
"contentLength": state.contentLength,
|
|
279
|
+
"method": request.method,
|
|
280
|
+
"path": request.path,
|
|
281
|
+
]
|
|
282
|
+
if let requestId {
|
|
283
|
+
uploadReceivedTraceValues["requestId"] = requestId
|
|
284
|
+
}
|
|
285
|
+
traceWifiInput("photo_receiver_upload_received", connection: connection, state: state, values: uploadReceivedTraceValues)
|
|
286
|
+
var uploadResponseTraceValues: [String: Any] = [
|
|
287
|
+
"statusCode": 200,
|
|
288
|
+
"success": true,
|
|
289
|
+
"durationMs": Int(Date().timeIntervalSince1970 * 1000 - state.startMs),
|
|
290
|
+
"contentLength": state.contentLength,
|
|
291
|
+
"method": request.method,
|
|
292
|
+
"path": request.path,
|
|
293
|
+
]
|
|
294
|
+
if let requestId {
|
|
295
|
+
uploadResponseTraceValues["requestId"] = requestId
|
|
296
|
+
}
|
|
297
|
+
traceWifiOutput("photo_receiver_response", connection: connection, state: state, values: uploadResponseTraceValues)
|
|
266
298
|
writeJson(
|
|
267
299
|
connection,
|
|
268
300
|
status: 200,
|
|
@@ -370,6 +402,51 @@ final class LocalPhotoUploadServer {
|
|
|
370
402
|
}
|
|
371
403
|
}
|
|
372
404
|
|
|
405
|
+
private func traceWifiInput(
|
|
406
|
+
_ type: String,
|
|
407
|
+
connection: NWConnection,
|
|
408
|
+
state: RequestReadState?,
|
|
409
|
+
values: [String: Any] = [:]
|
|
410
|
+
) {
|
|
411
|
+
traceWifi(direction: "wifi_to_phone", layer: "wifi_http_input", type: type, connection: connection, state: state, values: values)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private func traceWifiOutput(
|
|
415
|
+
_ type: String,
|
|
416
|
+
connection: NWConnection,
|
|
417
|
+
state: RequestReadState?,
|
|
418
|
+
values: [String: Any] = [:]
|
|
419
|
+
) {
|
|
420
|
+
traceWifi(direction: "phone_to_wifi", layer: "wifi_http_output", type: type, connection: connection, state: state, values: values)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private func traceWifi(
|
|
424
|
+
direction: String,
|
|
425
|
+
layer: String,
|
|
426
|
+
type: String,
|
|
427
|
+
connection: NWConnection,
|
|
428
|
+
state: RequestReadState?,
|
|
429
|
+
values: [String: Any]
|
|
430
|
+
) {
|
|
431
|
+
var payload = endpointPayload(type: type, connection: connection)
|
|
432
|
+
if let state {
|
|
433
|
+
payload["startMs"] = state.startMs
|
|
434
|
+
}
|
|
435
|
+
for (key, value) in values {
|
|
436
|
+
payload[key] = value
|
|
437
|
+
}
|
|
438
|
+
BleTraceLogger.logMap(direction: direction, layer: layer, type: type, payload: payload)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private func endpointPayload(type: String, connection: NWConnection) -> [String: Any] {
|
|
442
|
+
var payload: [String: Any] = ["type": type]
|
|
443
|
+
if case let .hostPort(host, port) = connection.endpoint {
|
|
444
|
+
payload["remoteHost"] = String(describing: host)
|
|
445
|
+
payload["remotePort"] = Int(port.rawValue)
|
|
446
|
+
}
|
|
447
|
+
return payload
|
|
448
|
+
}
|
|
449
|
+
|
|
373
450
|
private struct HttpRequest {
|
|
374
451
|
let method: String
|
|
375
452
|
let path: String
|
|
@@ -613,6 +690,7 @@ final class LocalPhotoUploadServer {
|
|
|
613
690
|
}
|
|
614
691
|
|
|
615
692
|
private final class RequestReadState {
|
|
693
|
+
let startMs = Date().timeIntervalSince1970 * 1000
|
|
616
694
|
var buffer = Data()
|
|
617
695
|
var request: HttpRequest?
|
|
618
696
|
var contentLength = 0
|
|
@@ -79,6 +79,16 @@ public class MentraPhotoReceiverModule: Module {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
private func handlePhotoUpload(_ upload: PhotoUpload) {
|
|
82
|
+
BleTraceLogger.logMap(
|
|
83
|
+
direction: "phone_to_app",
|
|
84
|
+
layer: "photo_receiver_event",
|
|
85
|
+
type: "photo_upload",
|
|
86
|
+
payload: [
|
|
87
|
+
"requestId": upload.requestId ?? "",
|
|
88
|
+
"fileName": upload.photoFile.lastPathComponent,
|
|
89
|
+
"byteCount": upload.byteCount,
|
|
90
|
+
]
|
|
91
|
+
)
|
|
82
92
|
sendEvent("photoUpload", [
|
|
83
93
|
"requestId": upload.requestId as Any,
|
|
84
94
|
"fileUri": upload.photoFile.absoluteString,
|
|
@@ -3,45 +3,19 @@ import Foundation
|
|
|
3
3
|
/// Defaults for the public Bluetooth SDK surface.
|
|
4
4
|
enum BluetoothSdkDefaults {
|
|
5
5
|
static var sdkVersion: String? {
|
|
6
|
-
|
|
7
|
-
normalizedSdkVersion(swiftPackageSdkVersion)
|
|
8
|
-
#else
|
|
9
|
-
packageVersion(from: sdkBundle)
|
|
10
|
-
#endif
|
|
6
|
+
normalizedSdkVersion(swiftPackageSdkVersion)
|
|
11
7
|
}
|
|
12
8
|
|
|
13
9
|
static let voiceActivityDetectionEnabled = false
|
|
14
|
-
private static let swiftPackageSdkVersion = "
|
|
10
|
+
private static let swiftPackageSdkVersion = "0.1.18"
|
|
15
11
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
16
12
|
|
|
17
|
-
private static var sdkBundle: Bundle {
|
|
18
|
-
#if SWIFT_PACKAGE
|
|
19
|
-
Bundle.module
|
|
20
|
-
#else
|
|
21
|
-
Bundle(for: BluetoothSdkBundleToken.self)
|
|
22
|
-
#endif
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private static func packageVersion(from bundle: Bundle) -> String? {
|
|
26
|
-
let version = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
|
27
|
-
let build = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String
|
|
28
|
-
|
|
29
|
-
for candidate in [version, build] {
|
|
30
|
-
if let sdkVersion = normalizedSdkVersion(candidate) {
|
|
31
|
-
return sdkVersion
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return nil
|
|
36
|
-
}
|
|
37
|
-
|
|
38
13
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
39
14
|
guard let value else {
|
|
40
15
|
return nil
|
|
41
16
|
}
|
|
42
17
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
43
18
|
guard !trimmed.isEmpty,
|
|
44
|
-
trimmed != "1.0",
|
|
45
19
|
trimmed != swiftPackageSdkVersionPlaceholder
|
|
46
20
|
else {
|
|
47
21
|
return nil
|
|
@@ -49,5 +23,3 @@ enum BluetoothSdkDefaults {
|
|
|
49
23
|
return trimmed
|
|
50
24
|
}
|
|
51
25
|
}
|
|
52
|
-
|
|
53
|
-
private final class BluetoothSdkBundleToken {}
|
package/ios/Source/Bridge.swift
CHANGED
|
@@ -15,6 +15,17 @@ class Bridge {
|
|
|
15
15
|
private static let micChannels = 1
|
|
16
16
|
private static let lc3FrameDurationMs = 10
|
|
17
17
|
private static let defaultLc3FrameSizeBytes = 60
|
|
18
|
+
private static let audioTraceMetadataKeys = [
|
|
19
|
+
"sampleRate",
|
|
20
|
+
"bitsPerSample",
|
|
21
|
+
"channels",
|
|
22
|
+
"encoding",
|
|
23
|
+
"frameDurationMs",
|
|
24
|
+
"frameSizeBytes",
|
|
25
|
+
"bitrate",
|
|
26
|
+
"packetizedFromGlasses",
|
|
27
|
+
"voiceActivityDetectionEnabled",
|
|
28
|
+
]
|
|
18
29
|
private static let eventSinkLock = NSLock()
|
|
19
30
|
private static let defaultEventSinkId = "default"
|
|
20
31
|
private static var eventSinks: [String: (String, [String: Any]) -> Void] = [:]
|
|
@@ -486,7 +497,59 @@ class Bridge {
|
|
|
486
497
|
static func sendTypedMessage(_ type: String, body: [String: Any]) {
|
|
487
498
|
var body = body
|
|
488
499
|
body["type"] = type
|
|
500
|
+
if let tracePayload = tracePayloadForTypedMessage(type, body: body) {
|
|
501
|
+
BleTraceLogger.logMap(
|
|
502
|
+
direction: "phone_to_app",
|
|
503
|
+
layer: "sdk_event_dispatch",
|
|
504
|
+
type: type,
|
|
505
|
+
payload: tracePayload
|
|
506
|
+
)
|
|
507
|
+
}
|
|
489
508
|
// Send directly using type as event name - no JSON serialization
|
|
490
509
|
dispatchEvent(type, body)
|
|
491
510
|
}
|
|
511
|
+
|
|
512
|
+
private static func tracePayloadForTypedMessage(_ type: String, body: [String: Any]) -> [String: Any]? {
|
|
513
|
+
if type == "log" {
|
|
514
|
+
return nil
|
|
515
|
+
}
|
|
516
|
+
if isAudioPayloadEvent(type) {
|
|
517
|
+
return audioTracePayload(type, body: body)
|
|
518
|
+
}
|
|
519
|
+
return body
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private static func isAudioPayloadEvent(_ type: String) -> Bool {
|
|
523
|
+
type == "mic_pcm" || type == "mic_lc3"
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private static func audioTracePayload(_ type: String, body: [String: Any]) -> [String: Any] {
|
|
527
|
+
var payload: [String: Any] = [
|
|
528
|
+
"type": type,
|
|
529
|
+
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
530
|
+
"payloadOmitted": true,
|
|
531
|
+
"payloadOmittedReason": "audio",
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
switch type {
|
|
535
|
+
case "mic_pcm":
|
|
536
|
+
if let data = body["pcm"] as? Data {
|
|
537
|
+
payload["audioBytes"] = data.count
|
|
538
|
+
}
|
|
539
|
+
case "mic_lc3":
|
|
540
|
+
if let data = body["lc3"] as? Data {
|
|
541
|
+
payload["audioBytes"] = data.count
|
|
542
|
+
}
|
|
543
|
+
default:
|
|
544
|
+
break
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for key in audioTraceMetadataKeys {
|
|
548
|
+
if let value = body[key] {
|
|
549
|
+
payload[key] = value
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return payload
|
|
554
|
+
}
|
|
492
555
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os.log
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
enum BleTraceLogger {
|
|
6
|
+
private static let log = OSLog(subsystem: "com.mentra.bluetoothsdk", category: "MentraBleTrace")
|
|
7
|
+
private static let maxPayloadChars = 3000
|
|
8
|
+
private static let k900Type = "k900"
|
|
9
|
+
private static let sensitiveKeyParts = ["password", "pass", "token", "secret", "authorization", "auth", "email"]
|
|
10
|
+
|
|
11
|
+
static func logJson(
|
|
12
|
+
direction: String,
|
|
13
|
+
layer: String,
|
|
14
|
+
payload: [String: Any]?,
|
|
15
|
+
bytes: Int? = nil,
|
|
16
|
+
sourceFile: String = #fileID,
|
|
17
|
+
sourceFunction: String = #function,
|
|
18
|
+
sourceLine: Int = #line
|
|
19
|
+
) {
|
|
20
|
+
guard let payload else {
|
|
21
|
+
emit(format(
|
|
22
|
+
direction: direction,
|
|
23
|
+
layer: layer,
|
|
24
|
+
source: source(file: sourceFile, function: sourceFunction, line: sourceLine),
|
|
25
|
+
type: "null",
|
|
26
|
+
bytes: bytes,
|
|
27
|
+
payload: "null"
|
|
28
|
+
))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let sanitized = sanitizeDictionary(payload)
|
|
33
|
+
emit(format(
|
|
34
|
+
direction: direction,
|
|
35
|
+
layer: layer,
|
|
36
|
+
source: source(file: sourceFile, function: sourceFunction, line: sourceLine),
|
|
37
|
+
type: extractType(from: payload),
|
|
38
|
+
bytes: bytes,
|
|
39
|
+
payload: jsonString(sanitized)
|
|
40
|
+
))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static func logMap(
|
|
44
|
+
direction: String,
|
|
45
|
+
layer: String,
|
|
46
|
+
type: String?,
|
|
47
|
+
payload: [String: Any],
|
|
48
|
+
bytes: Int? = nil,
|
|
49
|
+
sourceFile: String = #fileID,
|
|
50
|
+
sourceFunction: String = #function,
|
|
51
|
+
sourceLine: Int = #line
|
|
52
|
+
) {
|
|
53
|
+
let sanitized = sanitizeDictionary(payload)
|
|
54
|
+
emit(format(
|
|
55
|
+
direction: direction,
|
|
56
|
+
layer: layer,
|
|
57
|
+
source: source(file: sourceFile, function: sourceFunction, line: sourceLine),
|
|
58
|
+
type: type ?? extractType(from: sanitized),
|
|
59
|
+
bytes: bytes,
|
|
60
|
+
payload: jsonString(sanitized)
|
|
61
|
+
))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static func logLifecycle(component: String, event: String, extra: [String: Any] = [:]) {
|
|
65
|
+
var payload: [String: Any] = [
|
|
66
|
+
"event": event,
|
|
67
|
+
"component": component,
|
|
68
|
+
"pid": ProcessInfo.processInfo.processIdentifier,
|
|
69
|
+
"model": UIDevice.current.model,
|
|
70
|
+
"systemVersion": UIDevice.current.systemVersion,
|
|
71
|
+
]
|
|
72
|
+
for (key, value) in extra {
|
|
73
|
+
payload[key] = value
|
|
74
|
+
}
|
|
75
|
+
logMap(direction: "phone_app", layer: "app_lifecycle", type: event, payload: payload)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private static func emit(_ line: String) {
|
|
79
|
+
os_log("%{public}@", log: log, type: .info, line)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private static func format(
|
|
83
|
+
direction: String,
|
|
84
|
+
layer: String,
|
|
85
|
+
source: String,
|
|
86
|
+
type: String,
|
|
87
|
+
bytes: Int?,
|
|
88
|
+
payload: String
|
|
89
|
+
) -> String {
|
|
90
|
+
let bytesText = bytes.map { " bytes=\($0)" } ?? ""
|
|
91
|
+
return "BLE_TRACE direction=\(direction) layer=\(layer) source=\(source) type=\(type)\(bytesText) payload=\(truncate(payload))"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private static func extractType(from payload: [String: Any]) -> String {
|
|
95
|
+
if let type = payload["type"] as? String, !type.isEmpty {
|
|
96
|
+
return type
|
|
97
|
+
}
|
|
98
|
+
if let cValue = payload["C"] as? String, !cValue.isEmpty {
|
|
99
|
+
if let data = cValue.data(using: .utf8),
|
|
100
|
+
let inner = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
101
|
+
let type = inner["type"] as? String,
|
|
102
|
+
!type.isEmpty
|
|
103
|
+
{
|
|
104
|
+
return type
|
|
105
|
+
}
|
|
106
|
+
return extractK900Type(cValue)
|
|
107
|
+
}
|
|
108
|
+
return "unknown"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static func extractK900Type(_ value: String) -> String {
|
|
112
|
+
value == "sr_log" ? "\(k900Type):sr_log" : k900Type
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private static func sanitizeDictionary(_ value: [String: Any]) -> [String: Any] {
|
|
116
|
+
var output: [String: Any] = [:]
|
|
117
|
+
for (key, child) in value {
|
|
118
|
+
output[key] = sanitizeValue(key: key, value: child)
|
|
119
|
+
}
|
|
120
|
+
return output
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private static func sanitizeArray(_ value: [Any]) -> [Any] {
|
|
124
|
+
value.map { sanitizeValue(key: nil, value: $0) }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private static func sanitizeValue(key: String?, value: Any) -> Any {
|
|
128
|
+
if let key, sensitiveKeyParts.contains(where: { key.localizedCaseInsensitiveContains($0) }) {
|
|
129
|
+
return "<redacted>"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if key == "C", let stringValue = value as? String {
|
|
133
|
+
if let data = stringValue.data(using: .utf8),
|
|
134
|
+
let parsed = try? JSONSerialization.jsonObject(with: data)
|
|
135
|
+
{
|
|
136
|
+
return jsonString(sanitizeJsonValue(parsed))
|
|
137
|
+
}
|
|
138
|
+
return sanitizeK900Command(stringValue)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return sanitizeJsonValue(value)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static func sanitizeJsonValue(_ value: Any) -> Any {
|
|
145
|
+
switch value {
|
|
146
|
+
case let dictionary as [String: Any]:
|
|
147
|
+
return sanitizeDictionary(dictionary)
|
|
148
|
+
case let array as [Any]:
|
|
149
|
+
return sanitizeArray(array)
|
|
150
|
+
case let data as Data:
|
|
151
|
+
return "<data \(data.count) bytes>"
|
|
152
|
+
case let url as URL:
|
|
153
|
+
return url.absoluteString
|
|
154
|
+
case let bool as Bool:
|
|
155
|
+
return bool
|
|
156
|
+
case let number as NSNumber:
|
|
157
|
+
return number
|
|
158
|
+
case let string as String:
|
|
159
|
+
return string
|
|
160
|
+
case _ as NSNull:
|
|
161
|
+
return NSNull()
|
|
162
|
+
default:
|
|
163
|
+
return String(describing: value)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private static func sanitizeK900Command(_ value: String) -> String {
|
|
168
|
+
value == "sr_log" ? value : "<non-json C payload>"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static func jsonString(_ value: Any) -> String {
|
|
172
|
+
guard JSONSerialization.isValidJSONObject(value),
|
|
173
|
+
let data = try? JSONSerialization.data(withJSONObject: value),
|
|
174
|
+
let string = String(data: data, encoding: .utf8)
|
|
175
|
+
else {
|
|
176
|
+
return String(describing: value)
|
|
177
|
+
}
|
|
178
|
+
return string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private static func source(file: String, function: String, line: Int) -> String {
|
|
182
|
+
"\(function)(\(file):\(line))"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static func truncate(_ value: String) -> String {
|
|
186
|
+
guard value.count > maxPayloadChars else {
|
|
187
|
+
return value
|
|
188
|
+
}
|
|
189
|
+
let endIndex = value.index(value.startIndex, offsetBy: maxPayloadChars)
|
|
190
|
+
return "\(value[..<endIndex])...(truncated \(value.count - maxPayloadChars) chars)"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -1134,9 +1134,13 @@ extension MentraLive: CBPeripheralDelegate {
|
|
|
1134
1134
|
|
|
1135
1135
|
nonisolated func peripheral(_: CBPeripheral, didWriteValueFor _: CBCharacteristic, error: Error?) {
|
|
1136
1136
|
let errorDescription = error?.localizedDescription
|
|
1137
|
-
DispatchQueue.main.async {
|
|
1137
|
+
DispatchQueue.main.async { [weak self] in
|
|
1138
1138
|
if let errorDescription {
|
|
1139
1139
|
Bridge.log("LIVE: Error writing characteristic: \(errorDescription)")
|
|
1140
|
+
self?.logBleWriteTrace("write_callback", self?.pending?.trace, extra: [
|
|
1141
|
+
"success": false,
|
|
1142
|
+
"errorMessage": errorDescription,
|
|
1143
|
+
])
|
|
1140
1144
|
} else {
|
|
1141
1145
|
Bridge.log("LIVE: Characteristic write successful")
|
|
1142
1146
|
}
|
|
@@ -1358,6 +1362,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1358
1362
|
private let SIGNAL_STRENGTH_READ_INTERVAL_MS: TimeInterval = 10.0
|
|
1359
1363
|
private let MIN_SEND_DELAY_MS: UInt64 = 160_000_000 // 160ms in nanoseconds
|
|
1360
1364
|
private let READINESS_CHECK_INTERVAL_MS: TimeInterval = 2.5 // 2.5 seconds
|
|
1365
|
+
private let SIGNIFICANT_BLE_TRACE_DELAY_MS: TimeInterval = 250
|
|
1366
|
+
private let SIGNIFICANT_BLE_TRACE_QUEUE_SIZE = 5
|
|
1361
1367
|
|
|
1362
1368
|
/// Device Settings Keys
|
|
1363
1369
|
private let PREFS_DEVICE_NAME = "MentraLiveLastConnectedDeviceName"
|
|
@@ -1385,6 +1391,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1385
1391
|
private var isNewVersion = false
|
|
1386
1392
|
private var globalMessageId = 0
|
|
1387
1393
|
private var lastReceivedMessageId = 0
|
|
1394
|
+
private var bleWriteTraceSequence = 0
|
|
1388
1395
|
|
|
1389
1396
|
private var fullyBooted: Bool {
|
|
1390
1397
|
get { DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false }
|
|
@@ -1741,15 +1748,40 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1741
1748
|
// MARK: - Command Queue
|
|
1742
1749
|
|
|
1743
1750
|
class PendingMessage {
|
|
1744
|
-
init(data: Data, id: String, retries: Int) {
|
|
1751
|
+
init(data: Data, id: String, retries: Int, trace: BleWriteTrace? = nil) {
|
|
1745
1752
|
self.data = data
|
|
1746
1753
|
self.id = id
|
|
1747
1754
|
self.retries = retries
|
|
1755
|
+
self.trace = trace
|
|
1748
1756
|
}
|
|
1749
1757
|
|
|
1750
1758
|
let data: Data
|
|
1751
1759
|
let retries: Int
|
|
1752
1760
|
let id: String
|
|
1761
|
+
let trace: BleWriteTrace?
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
struct OutgoingBleCommandTraceInfo {
|
|
1765
|
+
let commandType: String
|
|
1766
|
+
let requestId: String?
|
|
1767
|
+
let appId: String?
|
|
1768
|
+
let messageId: Int64?
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
struct BleWriteTrace {
|
|
1772
|
+
let sequence: Int
|
|
1773
|
+
let commandType: String
|
|
1774
|
+
let requestId: String?
|
|
1775
|
+
let appId: String?
|
|
1776
|
+
let messageId: Int64?
|
|
1777
|
+
let chunkId: String?
|
|
1778
|
+
let chunkIndex: Int?
|
|
1779
|
+
let totalChunks: Int?
|
|
1780
|
+
let payloadBytes: Int?
|
|
1781
|
+
let packedBytes: Int
|
|
1782
|
+
let wakeup: Bool
|
|
1783
|
+
let chunked: Bool
|
|
1784
|
+
let queuedAtMs: TimeInterval
|
|
1753
1785
|
}
|
|
1754
1786
|
|
|
1755
1787
|
private var pending: PendingMessage?
|
|
@@ -1758,12 +1790,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1758
1790
|
actor CommandQueue {
|
|
1759
1791
|
private var commands: [PendingMessage] = []
|
|
1760
1792
|
|
|
1761
|
-
func enqueue(_ command: PendingMessage) {
|
|
1793
|
+
func enqueue(_ command: PendingMessage) -> Int {
|
|
1762
1794
|
commands.append(command)
|
|
1795
|
+
return commands.count
|
|
1763
1796
|
}
|
|
1764
1797
|
|
|
1765
|
-
func pushToFront(_ command: PendingMessage) {
|
|
1798
|
+
func pushToFront(_ command: PendingMessage) -> Int {
|
|
1766
1799
|
commands.insert(command, at: 0)
|
|
1800
|
+
return commands.count
|
|
1767
1801
|
}
|
|
1768
1802
|
|
|
1769
1803
|
func dequeue() -> PendingMessage? {
|
|
@@ -1791,18 +1825,28 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1791
1825
|
guard let peripheral = connectedPeripheral,
|
|
1792
1826
|
let txChar = txCharacteristic
|
|
1793
1827
|
else {
|
|
1828
|
+
logBleWriteTrace("write_dropped", message.trace, extra: [
|
|
1829
|
+
"errorMessage": "missing peripheral or TX characteristic",
|
|
1830
|
+
])
|
|
1794
1831
|
return
|
|
1795
1832
|
}
|
|
1796
1833
|
|
|
1797
1834
|
// Enforce rate limiting
|
|
1798
1835
|
let currentTime = Date().timeIntervalSince1970 * 1000
|
|
1799
1836
|
let timeSinceLastSend = currentTime - lastSendTimeMs
|
|
1837
|
+
let queueDelayMs = message.trace.map { currentTime - $0.queuedAtMs }
|
|
1800
1838
|
|
|
1801
1839
|
try? await Task.sleep(nanoseconds: UInt64(1_000_000))
|
|
1802
1840
|
lastSendTimeMs = Date().timeIntervalSince1970 * 1000
|
|
1803
1841
|
|
|
1804
1842
|
// Send the data
|
|
1805
1843
|
peripheral.writeValue(message.data, for: txChar, type: .withResponse)
|
|
1844
|
+
logBleWriteTrace("write_requested", message.trace, extra: [
|
|
1845
|
+
"queueDelayMs": queueDelayMs,
|
|
1846
|
+
"timeSinceLastSendMs": timeSinceLastSend,
|
|
1847
|
+
"currentMtu": currentMtu,
|
|
1848
|
+
"writeType": "withResponse",
|
|
1849
|
+
])
|
|
1806
1850
|
|
|
1807
1851
|
// don't do the retry system on the old glasses versions
|
|
1808
1852
|
if !isNewVersion {
|
|
@@ -1844,12 +1888,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1844
1888
|
let retryMessage = PendingMessage(
|
|
1845
1889
|
data: pendingMessage.data,
|
|
1846
1890
|
id: pendingMessage.id,
|
|
1847
|
-
retries: pendingMessage.retries + 1
|
|
1891
|
+
retries: pendingMessage.retries + 1,
|
|
1892
|
+
trace: pendingMessage.trace
|
|
1848
1893
|
)
|
|
1849
1894
|
|
|
1850
1895
|
// Push to front of queue for immediate retry
|
|
1851
1896
|
Task {
|
|
1852
|
-
await self.commandQueue.pushToFront(retryMessage)
|
|
1897
|
+
let queueSize = await self.commandQueue.pushToFront(retryMessage)
|
|
1898
|
+
await MainActor.run {
|
|
1899
|
+
self.logBleWriteTrace("retry_queued", retryMessage.trace, extra: [
|
|
1900
|
+
"retryDelayMs": 0,
|
|
1901
|
+
"queueSizeAfterAdd": queueSize,
|
|
1902
|
+
])
|
|
1903
|
+
}
|
|
1853
1904
|
}
|
|
1854
1905
|
|
|
1855
1906
|
Bridge.log(
|
|
@@ -2102,6 +2153,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2102
2153
|
private func processJsonObject(_ json: [String: Any]) {
|
|
2103
2154
|
// Log ALL incoming JSON objects for debugging
|
|
2104
2155
|
// Bridge.log("LIVE: DEBUG: processJsonObject: \(json)")
|
|
2156
|
+
BleTraceLogger.logJson(direction: "glasses_to_phone", layer: "sdk_ble_event", payload: json)
|
|
2105
2157
|
|
|
2106
2158
|
if MessageChunker.isChunkedMessage(json) {
|
|
2107
2159
|
processChunkedJsonObject(json)
|
|
@@ -3651,8 +3703,211 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3651
3703
|
// MARK: - Sending Data
|
|
3652
3704
|
|
|
3653
3705
|
func queueSend(_ data: Data, id: String) {
|
|
3706
|
+
queueSend(data, id: id, trace: nil)
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
func queueSend(_ data: Data, id: String, trace: BleWriteTrace?) {
|
|
3710
|
+
let significantQueueSize = SIGNIFICANT_BLE_TRACE_QUEUE_SIZE
|
|
3654
3711
|
Task {
|
|
3655
|
-
await commandQueue.enqueue(PendingMessage(data: data, id: id, retries: 0))
|
|
3712
|
+
let queueSize = await commandQueue.enqueue(PendingMessage(data: data, id: id, retries: 0, trace: trace))
|
|
3713
|
+
guard trace != nil, queueSize >= significantQueueSize else {
|
|
3714
|
+
return
|
|
3715
|
+
}
|
|
3716
|
+
await MainActor.run {
|
|
3717
|
+
self.logBleChunkTrace("queued", trace, extra: [
|
|
3718
|
+
"queueSizeAfterAdd": queueSize,
|
|
3719
|
+
])
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
private func outgoingBleCommandTraceInfo(_ json: [String: Any]) -> OutgoingBleCommandTraceInfo {
|
|
3725
|
+
OutgoingBleCommandTraceInfo(
|
|
3726
|
+
commandType: nonBlankString(json["type"]) ?? "unknown",
|
|
3727
|
+
requestId: nonBlankString(json["requestId"]),
|
|
3728
|
+
appId: nonBlankString(json["appId"]),
|
|
3729
|
+
messageId: traceInt64(json["mId"])
|
|
3730
|
+
)
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
private func createBleWriteTrace(
|
|
3734
|
+
commandInfo: OutgoingBleCommandTraceInfo,
|
|
3735
|
+
chunkId: String?,
|
|
3736
|
+
chunkIndex: Int?,
|
|
3737
|
+
totalChunks: Int?,
|
|
3738
|
+
payloadBytes: Int?,
|
|
3739
|
+
packedBytes: Int,
|
|
3740
|
+
wakeup: Bool,
|
|
3741
|
+
chunked: Bool
|
|
3742
|
+
) -> BleWriteTrace {
|
|
3743
|
+
let sequence = bleWriteTraceSequence
|
|
3744
|
+
bleWriteTraceSequence += 1
|
|
3745
|
+
return BleWriteTrace(
|
|
3746
|
+
sequence: sequence,
|
|
3747
|
+
commandType: commandInfo.commandType,
|
|
3748
|
+
requestId: commandInfo.requestId,
|
|
3749
|
+
appId: commandInfo.appId,
|
|
3750
|
+
messageId: commandInfo.messageId,
|
|
3751
|
+
chunkId: chunkId,
|
|
3752
|
+
chunkIndex: chunkIndex,
|
|
3753
|
+
totalChunks: totalChunks,
|
|
3754
|
+
payloadBytes: payloadBytes,
|
|
3755
|
+
packedBytes: packedBytes,
|
|
3756
|
+
wakeup: wakeup,
|
|
3757
|
+
chunked: chunked,
|
|
3758
|
+
queuedAtMs: Date().timeIntervalSince1970 * 1000
|
|
3759
|
+
)
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
private func logBleChunkTrace(
|
|
3763
|
+
_ stage: String,
|
|
3764
|
+
_ trace: BleWriteTrace?,
|
|
3765
|
+
extra: [String: Any?] = [:]
|
|
3766
|
+
) {
|
|
3767
|
+
logBleTrace(layer: "sdk_ble_chunk", stage: stage, trace: trace, extra: extra)
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
private func logBleWriteTrace(
|
|
3771
|
+
_ stage: String,
|
|
3772
|
+
_ trace: BleWriteTrace?,
|
|
3773
|
+
extra: [String: Any?] = [:]
|
|
3774
|
+
) {
|
|
3775
|
+
logBleTrace(layer: "sdk_ble_write", stage: stage, trace: trace, extra: extra)
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
private func logBleTrace(
|
|
3779
|
+
layer: String,
|
|
3780
|
+
stage: String,
|
|
3781
|
+
trace: BleWriteTrace?,
|
|
3782
|
+
extra: [String: Any?] = [:]
|
|
3783
|
+
) {
|
|
3784
|
+
guard let trace,
|
|
3785
|
+
let warningReason = bleTraceWarningReason(stage: stage, extra: extra)
|
|
3786
|
+
else {
|
|
3787
|
+
return
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
var payload: [String: Any] = [
|
|
3791
|
+
"level": "warning",
|
|
3792
|
+
"warningReason": warningReason,
|
|
3793
|
+
"stage": stage,
|
|
3794
|
+
"sequence": trace.sequence,
|
|
3795
|
+
"commandType": trace.commandType,
|
|
3796
|
+
"packedBytes": trace.packedBytes,
|
|
3797
|
+
"wakeup": trace.wakeup,
|
|
3798
|
+
"chunked": trace.chunked,
|
|
3799
|
+
"queuedAtMs": trace.queuedAtMs,
|
|
3800
|
+
]
|
|
3801
|
+
if let requestId = trace.requestId { payload["requestId"] = requestId }
|
|
3802
|
+
if let appId = trace.appId { payload["appId"] = appId }
|
|
3803
|
+
if let messageId = trace.messageId { payload["messageId"] = messageId }
|
|
3804
|
+
if let chunkId = trace.chunkId { payload["chunkId"] = chunkId }
|
|
3805
|
+
if let chunkIndex = trace.chunkIndex {
|
|
3806
|
+
payload["chunkIndex"] = chunkIndex
|
|
3807
|
+
payload["chunkNumber"] = chunkIndex + 1
|
|
3808
|
+
}
|
|
3809
|
+
if let totalChunks = trace.totalChunks { payload["totalChunks"] = totalChunks }
|
|
3810
|
+
if let payloadBytes = trace.payloadBytes { payload["payloadBytes"] = payloadBytes }
|
|
3811
|
+
for (key, value) in extra {
|
|
3812
|
+
if let value {
|
|
3813
|
+
payload[key] = value
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
BleTraceLogger.logMap(direction: "phone_to_glasses", layer: layer, type: trace.commandType, payload: payload)
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
private func bleTraceWarningReason(stage: String, extra: [String: Any?]) -> String? {
|
|
3821
|
+
if extraValue(extra, "errorClass") != nil || extraValue(extra, "errorMessage") != nil {
|
|
3822
|
+
return "ble_write_error"
|
|
3823
|
+
}
|
|
3824
|
+
if let success = extraValue(extra, "success") as? Bool, !success {
|
|
3825
|
+
return "ble_write_failed"
|
|
3826
|
+
}
|
|
3827
|
+
if let writeAccepted = extraValue(extra, "writeAccepted") as? Bool, !writeAccepted {
|
|
3828
|
+
return "ble_write_rejected"
|
|
3829
|
+
}
|
|
3830
|
+
if traceDouble(extraValue(extra, "queueDelayMs")).map({ $0 >= SIGNIFICANT_BLE_TRACE_DELAY_MS }) == true {
|
|
3831
|
+
return "queue_delay"
|
|
3832
|
+
}
|
|
3833
|
+
if traceDouble(extraValue(extra, "callbackDelayMs")).map({ $0 >= SIGNIFICANT_BLE_TRACE_DELAY_MS }) == true {
|
|
3834
|
+
return "write_callback_delay"
|
|
3835
|
+
}
|
|
3836
|
+
if traceDouble(extraValue(extra, "remainingDelayMs")).map({ $0 >= SIGNIFICANT_BLE_TRACE_DELAY_MS }) == true {
|
|
3837
|
+
return "rate_limit_delay"
|
|
3838
|
+
}
|
|
3839
|
+
if traceDouble(extraValue(extra, "nextProcessDelayMs")).map({ $0 >= SIGNIFICANT_BLE_TRACE_DELAY_MS }) == true {
|
|
3840
|
+
return "next_process_delay"
|
|
3841
|
+
}
|
|
3842
|
+
if extraValue(extra, "retryDelayMs") != nil {
|
|
3843
|
+
return "write_retry"
|
|
3844
|
+
}
|
|
3845
|
+
if stage == "queued",
|
|
3846
|
+
traceInt(extraValue(extra, "queueSizeAfterAdd")).map({ $0 >= SIGNIFICANT_BLE_TRACE_QUEUE_SIZE }) == true
|
|
3847
|
+
{
|
|
3848
|
+
return "queue_depth"
|
|
3849
|
+
}
|
|
3850
|
+
return nil
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
private func extraValue(_ extra: [String: Any?], _ key: String) -> Any? {
|
|
3854
|
+
extra[key] ?? nil
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
private func nonBlankString(_ value: Any?) -> String? {
|
|
3858
|
+
guard let value else { return nil }
|
|
3859
|
+
let string: String
|
|
3860
|
+
if let value = value as? String {
|
|
3861
|
+
string = value
|
|
3862
|
+
} else {
|
|
3863
|
+
string = String(describing: value)
|
|
3864
|
+
}
|
|
3865
|
+
return string.isEmpty ? nil : string
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
private func traceInt64(_ value: Any?) -> Int64? {
|
|
3869
|
+
guard let value else { return nil }
|
|
3870
|
+
switch value {
|
|
3871
|
+
case let value as Int64:
|
|
3872
|
+
return value
|
|
3873
|
+
case let value as Int:
|
|
3874
|
+
return Int64(value)
|
|
3875
|
+
case let value as NSNumber:
|
|
3876
|
+
return value.int64Value
|
|
3877
|
+
case let value as String:
|
|
3878
|
+
return Int64(value)
|
|
3879
|
+
default:
|
|
3880
|
+
return nil
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
private func traceInt(_ value: Any?) -> Int? {
|
|
3885
|
+
guard let value else { return nil }
|
|
3886
|
+
switch value {
|
|
3887
|
+
case let value as Int:
|
|
3888
|
+
return value
|
|
3889
|
+
case let value as NSNumber:
|
|
3890
|
+
return value.intValue
|
|
3891
|
+
case let value as String:
|
|
3892
|
+
return Int(value)
|
|
3893
|
+
default:
|
|
3894
|
+
return nil
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
private func traceDouble(_ value: Any?) -> Double? {
|
|
3899
|
+
guard let value else { return nil }
|
|
3900
|
+
switch value {
|
|
3901
|
+
case let value as Double:
|
|
3902
|
+
return value
|
|
3903
|
+
case let value as Int:
|
|
3904
|
+
return Double(value)
|
|
3905
|
+
case let value as NSNumber:
|
|
3906
|
+
return value.doubleValue
|
|
3907
|
+
case let value as String:
|
|
3908
|
+
return Double(value)
|
|
3909
|
+
default:
|
|
3910
|
+
return nil
|
|
3656
3911
|
}
|
|
3657
3912
|
}
|
|
3658
3913
|
|
|
@@ -3671,6 +3926,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3671
3926
|
|
|
3672
3927
|
let jsonData = try JSONSerialization.data(withJSONObject: json)
|
|
3673
3928
|
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
3929
|
+
let commandInfo = outgoingBleCommandTraceInfo(json)
|
|
3930
|
+
BleTraceLogger.logJson(
|
|
3931
|
+
direction: "phone_to_glasses",
|
|
3932
|
+
layer: "sdk_ble_command",
|
|
3933
|
+
payload: json,
|
|
3934
|
+
bytes: jsonData.count
|
|
3935
|
+
)
|
|
3936
|
+
|
|
3674
3937
|
// First check if the message needs chunking
|
|
3675
3938
|
// Create a test C-wrapped version to check size
|
|
3676
3939
|
var testWrapper: [String: Any] = [K900ProtocolUtils.FIELD_C: jsonString]
|
|
@@ -3707,7 +3970,21 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3707
3970
|
// All other chunks get "-1" (no ACK tracking)
|
|
3708
3971
|
let isFinalChunk = (index == chunks.count - 1)
|
|
3709
3972
|
let chunkTrackingId = (requireAck && isFinalChunk) ? trackingId : "-1"
|
|
3710
|
-
|
|
3973
|
+
let trace = createBleWriteTrace(
|
|
3974
|
+
commandInfo: commandInfo,
|
|
3975
|
+
chunkId: chunk["id"] as? String,
|
|
3976
|
+
chunkIndex: index,
|
|
3977
|
+
totalChunks: chunks.count,
|
|
3978
|
+
payloadBytes: chunkStr.data(using: .utf8)?.count,
|
|
3979
|
+
packedBytes: packedData.count,
|
|
3980
|
+
wakeup: wakeUp && index == 0,
|
|
3981
|
+
chunked: true
|
|
3982
|
+
)
|
|
3983
|
+
logBleChunkTrace("created", trace, extra: [
|
|
3984
|
+
"chunkJsonBytes": chunkStr.data(using: .utf8)?.count,
|
|
3985
|
+
"messageBytes": jsonData.count,
|
|
3986
|
+
])
|
|
3987
|
+
queueSend(packedData, id: chunkTrackingId, trace: trace)
|
|
3711
3988
|
|
|
3712
3989
|
// Add small delay between chunks to avoid overwhelming the connection
|
|
3713
3990
|
if index < chunks.count - 1 {
|
|
@@ -3724,7 +4001,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3724
4001
|
}
|
|
3725
4002
|
Bridge.log("LIVE: Sending data to glasses: \(jsonString)")
|
|
3726
4003
|
let packedData = packJson(jsonString, wakeUp: wakeUp) ?? Data()
|
|
3727
|
-
|
|
4004
|
+
let trace = createBleWriteTrace(
|
|
4005
|
+
commandInfo: commandInfo,
|
|
4006
|
+
chunkId: nil,
|
|
4007
|
+
chunkIndex: nil,
|
|
4008
|
+
totalChunks: nil,
|
|
4009
|
+
payloadBytes: jsonData.count,
|
|
4010
|
+
packedBytes: packedData.count,
|
|
4011
|
+
wakeup: wakeUp,
|
|
4012
|
+
chunked: false
|
|
4013
|
+
)
|
|
4014
|
+
logBleChunkTrace("created", trace, extra: [
|
|
4015
|
+
"messageBytes": jsonData.count,
|
|
4016
|
+
])
|
|
4017
|
+
queueSend(packedData, id: trackingId, trace: trace)
|
|
3728
4018
|
}
|
|
3729
4019
|
}
|
|
3730
4020
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "SDK for communicating with smart glasses",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"!ios/Pods/**",
|
|
60
60
|
"plugin/build",
|
|
61
61
|
"README.md",
|
|
62
|
+
"scripts",
|
|
62
63
|
"src",
|
|
63
64
|
"!src/__tests__"
|
|
64
65
|
],
|
|
@@ -68,6 +69,8 @@
|
|
|
68
69
|
"clean": "expo-module clean",
|
|
69
70
|
"test": "expo-module test",
|
|
70
71
|
"prepare": "expo-module prepare",
|
|
72
|
+
"prepack": "node scripts/inject-ios-sdk-version.mjs",
|
|
73
|
+
"postpack": "node scripts/inject-ios-sdk-version.mjs --restore",
|
|
71
74
|
"prepublishOnly": "expo-module prepublishOnly",
|
|
72
75
|
"lint": "expo-module lint",
|
|
73
76
|
"expo-module": "expo-module",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
8
|
+
const packageJsonPath = path.join(packageRoot, "package.json")
|
|
9
|
+
const sourcePath = path.join(packageRoot, "ios/Source/BluetoothSdkDefaults.swift")
|
|
10
|
+
const backupPath = path.join(packageRoot, ".bluetooth-sdk-version-prepack-backup")
|
|
11
|
+
const restore = process.argv.includes("--restore")
|
|
12
|
+
|
|
13
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"))
|
|
14
|
+
const version = packageJson.version
|
|
15
|
+
|
|
16
|
+
if (typeof version !== "string" || version.trim() === "") {
|
|
17
|
+
throw new Error("mobile/modules/bluetooth-sdk/package.json is missing a version")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const placeholder = 'private static let swiftPackageSdkVersion = "__MENTRA_BLUETOOTH_SDK_VERSION__"'
|
|
21
|
+
const injectedVersion = `private static let swiftPackageSdkVersion = "${version}"`
|
|
22
|
+
|
|
23
|
+
async function pathExists(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(filePath)
|
|
26
|
+
return true
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (restore) {
|
|
33
|
+
if (!(await pathExists(backupPath))) {
|
|
34
|
+
console.log("No iOS SDK version prepack backup found; nothing to restore.")
|
|
35
|
+
process.exit(0)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await fs.writeFile(sourcePath, await fs.readFile(backupPath, "utf8"))
|
|
39
|
+
await fs.rm(backupPath)
|
|
40
|
+
console.log("Restored placeholder iOS SDK version after packing.")
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (await pathExists(backupPath)) {
|
|
45
|
+
throw new Error("Found an existing iOS SDK version prepack backup. Run `npm run postpack` before packing again.")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const originalSource = await fs.readFile(sourcePath, "utf8")
|
|
49
|
+
let packedSource
|
|
50
|
+
|
|
51
|
+
if (originalSource.includes(placeholder)) {
|
|
52
|
+
packedSource = originalSource.replace(placeholder, injectedVersion)
|
|
53
|
+
} else if (originalSource.includes(injectedVersion)) {
|
|
54
|
+
packedSource = originalSource
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error("BluetoothSdkDefaults.swift does not contain the expected SDK version placeholder.")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(backupPath, originalSource)
|
|
60
|
+
|
|
61
|
+
if (packedSource !== originalSource) {
|
|
62
|
+
await fs.writeFile(sourcePath, packedSource)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`Injected iOS SDK version ${version} for npm packaging.`)
|