@mentra/bluetooth-sdk 0.1.21-beta.1 → 0.1.21-beta.3
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/DeviceManager.kt +6 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +15 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +3 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +6 -5
- package/ios/Source/DeviceStore.swift +3 -0
- package/ios/Source/sgcs/G2.swift +108 -41
- package/ios/Source/sgcs/MentraLive.swift +17 -14
- package/ios/Source/streaming/StreamModels.swift +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ enterprise device deployments:
|
|
|
158
158
|
|
|
159
159
|
- `bluetooth_sdk_started`: sent once per app runtime after the native SDK starts.
|
|
160
160
|
- `bluetooth_sdk_glasses_connected`: sent when SDK status transitions from not connected to connected.
|
|
161
|
-
- `bluetooth_sdk_glasses_identified`: sent once per connection after the SDK receives a valid manufacturing serial from the glasses. This fires for every supported model that reports a serial. G1 and Ar99 decode the serial from the glasses' BLE advertisement
|
|
161
|
+
- `bluetooth_sdk_glasses_identified`: sent once per connection after the SDK receives a valid manufacturing serial from the glasses. This fires for every supported model that reports a serial. G1 and Ar99 decode the serial from the glasses' BLE advertisement. Mentra Live reports the product serial provisioned by its Android firmware through `asg_client`.
|
|
162
162
|
|
|
163
163
|
Analytics delivery is fire-and-forget: events are submitted asynchronously, do
|
|
164
164
|
not block Bluetooth SDK behavior, and are not retried if delivery fails.
|
|
@@ -2050,11 +2050,13 @@ class DeviceManager {
|
|
|
2050
2050
|
shouldSendBootingMessage = true // Reset for next first connect
|
|
2051
2051
|
// clear glasses properties:
|
|
2052
2052
|
DeviceStore.apply("glasses", "deviceModel", "")
|
|
2053
|
-
//
|
|
2054
|
-
// previously connected pair
|
|
2055
|
-
// connection (e.g. switching from G1/Ar99, which populate it from the
|
|
2056
|
-
// advertisement, to a model that never writes it, like G2).
|
|
2053
|
+
// Device identifiers are session-bound. Clear them on every disconnect so a
|
|
2054
|
+
// previously connected pair can never be reported for the next connection.
|
|
2057
2055
|
DeviceStore.apply("glasses", "serialNumber", "")
|
|
2056
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", "")
|
|
2057
|
+
DeviceStore.apply("glasses", "leftMacAddress", "")
|
|
2058
|
+
DeviceStore.apply("glasses", "rightMacAddress", "")
|
|
2059
|
+
DeviceStore.apply("glasses", "macAddress", "")
|
|
2058
2060
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2059
2061
|
DeviceStore.apply("glasses", "connected", false)
|
|
2060
2062
|
DeviceStore.apply(
|
|
@@ -2164,4 +2166,3 @@ class DeviceManager {
|
|
|
2164
2166
|
}
|
|
2165
2167
|
}
|
|
2166
2168
|
}
|
|
2167
|
-
|
|
@@ -73,6 +73,10 @@ object DeviceStore {
|
|
|
73
73
|
store.set("glasses", "caseCharging", false)
|
|
74
74
|
store.set("glasses", "caseBatteryLevel", -1)
|
|
75
75
|
store.set("glasses", "headUp", false)
|
|
76
|
+
store.set("glasses", "bluetoothMacAddress", "")
|
|
77
|
+
store.set("glasses", "leftMacAddress", "")
|
|
78
|
+
store.set("glasses", "rightMacAddress", "")
|
|
79
|
+
store.set("glasses", "macAddress", "")
|
|
76
80
|
store.set("glasses", "serialNumber", "")
|
|
77
81
|
store.set("glasses", "style", "")
|
|
78
82
|
store.set("glasses", "color", "")
|
|
@@ -817,12 +817,13 @@ class MentraLive : SGCManager() {
|
|
|
817
817
|
}
|
|
818
818
|
|
|
819
819
|
if (state == ConnTypes.DISCONNECTED) {
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
// clear
|
|
823
|
-
// clearing on CONNECTED would wipe
|
|
824
|
-
// glasses_ready (e.g. ASG restart) re-publishes CONNECTED.
|
|
820
|
+
// Device identity is session-bound. Clear it on disconnect so a previous pair's
|
|
821
|
+
// identifiers can never be associated with the next connection. Connect must NOT
|
|
822
|
+
// clear them: DeviceManager.disconnect already wipes them before any new connection,
|
|
823
|
+
// and clearing on CONNECTED would wipe still-valid identity mid-session when a
|
|
824
|
+
// same-link glasses_ready (e.g. ASG restart) re-publishes CONNECTED.
|
|
825
825
|
DeviceStore.apply("glasses", "serialNumber", "")
|
|
826
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", "")
|
|
826
827
|
}
|
|
827
828
|
|
|
828
829
|
// Actually update the connection state!
|
|
@@ -3729,9 +3730,11 @@ class MentraLive : SGCManager() {
|
|
|
3729
3730
|
if (otaVersionUrlLegacy != null) otaVersionUrlLegacy else ""
|
|
3730
3731
|
)
|
|
3731
3732
|
DeviceStore.apply("glasses", "firmwareVersion", firmwareVersionLegacy)
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3733
|
+
btMacAddressLegacy.trim().takeIf { it.isNotEmpty() }?.let {
|
|
3734
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", it)
|
|
3735
|
+
}
|
|
3736
|
+
serialNumberLegacy.trim().takeIf { it.isNotEmpty() }?.let {
|
|
3737
|
+
DeviceStore.apply("glasses", "serialNumber", it)
|
|
3735
3738
|
}
|
|
3736
3739
|
|
|
3737
3740
|
val versionInfoLegacy = HashMap<String, Any>()
|
|
@@ -3994,16 +3997,11 @@ class MentraLive : SGCManager() {
|
|
|
3994
3997
|
fields["mtk_fw_version"] as String
|
|
3995
3998
|
)
|
|
3996
3999
|
}
|
|
3997
|
-
|
|
3998
|
-
DeviceStore.apply(
|
|
3999
|
-
"glasses",
|
|
4000
|
-
"bluetoothMacAddress",
|
|
4001
|
-
fields["bt_mac_address"] as String
|
|
4002
|
-
)
|
|
4000
|
+
(fields["bt_mac_address"] as? String)?.trim()?.takeIf { it.isNotEmpty() }?.let {
|
|
4001
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", it)
|
|
4003
4002
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
DeviceStore.apply("glasses", "serialNumber", serialNumber)
|
|
4003
|
+
(fields["serial_number"] as? String)?.trim()?.takeIf { it.isNotEmpty() }?.let {
|
|
4004
|
+
DeviceStore.apply("glasses", "serialNumber", it)
|
|
4007
4005
|
}
|
|
4008
4006
|
if (fields.containsKey("system_time_ms")) {
|
|
4009
4007
|
val v = fields["system_time_ms"]
|
|
@@ -206,6 +206,7 @@ data class StreamRequest @JvmOverloads constructor(
|
|
|
206
206
|
val sound: Boolean = true,
|
|
207
207
|
val video: StreamVideoConfig? = null,
|
|
208
208
|
val audio: StreamAudioConfig? = null,
|
|
209
|
+
val authToken: String? = null,
|
|
209
210
|
) {
|
|
210
211
|
fun toMap(): Map<String, Any> =
|
|
211
212
|
buildMap {
|
|
@@ -215,6 +216,7 @@ data class StreamRequest @JvmOverloads constructor(
|
|
|
215
216
|
put("sound", sound)
|
|
216
217
|
video?.toMap()?.takeIf { it.isNotEmpty() }?.let { put("video", it) }
|
|
217
218
|
audio?.toMap()?.takeIf { it.isNotEmpty() }?.let { put("audio", it) }
|
|
219
|
+
authToken?.takeIf { it.isNotEmpty() }?.let { put("authToken", it) }
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
companion object {
|
|
@@ -228,6 +230,7 @@ data class StreamRequest @JvmOverloads constructor(
|
|
|
228
230
|
sound = values["sound"] as? Boolean ?: true,
|
|
229
231
|
video = StreamVideoConfig.fromMap(stringMapValue(values["video"])),
|
|
230
232
|
audio = StreamAudioConfig.fromMap(stringMapValue(values["audio"])),
|
|
233
|
+
authToken = values["authToken"] as? String ?: values["auth_token"] as? String,
|
|
231
234
|
)
|
|
232
235
|
}
|
|
233
236
|
}
|
|
@@ -8,7 +8,7 @@ enum BluetoothSdkDefaults {
|
|
|
8
8
|
|
|
9
9
|
static let voiceActivityDetectionEnabled = false
|
|
10
10
|
static let loudnessGateEnabled = true
|
|
11
|
-
private static let swiftPackageSdkVersion = "0.1.21-beta.
|
|
11
|
+
private static let swiftPackageSdkVersion = "0.1.21-beta.3"
|
|
12
12
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
13
13
|
|
|
14
14
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -1712,11 +1712,13 @@ struct ViewState {
|
|
|
1712
1712
|
shouldSendBootingMessage = true // Reset for next first connect
|
|
1713
1713
|
// clear glasses properties:
|
|
1714
1714
|
DeviceStore.shared.apply("glasses", "deviceModel", "")
|
|
1715
|
-
//
|
|
1716
|
-
// previously connected pair
|
|
1717
|
-
// connection (e.g. switching from G1/Ar99, which populate it from the
|
|
1718
|
-
// advertisement, to a model that never writes it, like G2).
|
|
1715
|
+
// Device identifiers are session-bound. Clear them on every disconnect so a
|
|
1716
|
+
// previously connected pair can never be reported for the next connection.
|
|
1719
1717
|
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
1718
|
+
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", "")
|
|
1719
|
+
DeviceStore.shared.apply("glasses", "leftMacAddress", "")
|
|
1720
|
+
DeviceStore.shared.apply("glasses", "rightMacAddress", "")
|
|
1721
|
+
DeviceStore.shared.apply("glasses", "macAddress", "")
|
|
1720
1722
|
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
1721
1723
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
1722
1724
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
@@ -1807,4 +1809,3 @@ struct ViewState {
|
|
|
1807
1809
|
cancellables.removeAll()
|
|
1808
1810
|
}
|
|
1809
1811
|
}
|
|
1810
|
-
|
|
@@ -35,6 +35,9 @@ class DeviceStore {
|
|
|
35
35
|
store.set("glasses", "caseCharging", false)
|
|
36
36
|
store.set("glasses", "caseBatteryLevel", -1)
|
|
37
37
|
store.set("glasses", "headUp", false)
|
|
38
|
+
store.set("glasses", "bluetoothMacAddress", "")
|
|
39
|
+
store.set("glasses", "leftMacAddress", "")
|
|
40
|
+
store.set("glasses", "rightMacAddress", "")
|
|
38
41
|
store.set("glasses", "serialNumber", "")
|
|
39
42
|
store.set("glasses", "style", "")
|
|
40
43
|
store.set("glasses", "color", "")
|
package/ios/Source/sgcs/G2.swift
CHANGED
|
@@ -11,6 +11,14 @@ import CoreBluetooth
|
|
|
11
11
|
import Foundation
|
|
12
12
|
import UIKit
|
|
13
13
|
|
|
14
|
+
/// Keep a low-rate, non-audio EvenHub sensor stream active so iOS continues receiving BLE
|
|
15
|
+
/// notifications while the Mentra App is backgrounded. The samples stay internal unless IMU was
|
|
16
|
+
/// explicitly enabled through Mentra's developer-facing setting.
|
|
17
|
+
private let g2ImuKeepaliveEnabled = true
|
|
18
|
+
private let g2ImuKeepaliveReportPace: Int32 = 1000
|
|
19
|
+
private let g2ImuConfirmationDelay: TimeInterval = 2
|
|
20
|
+
private let g2ImuMaxControlAttempts = 3
|
|
21
|
+
|
|
14
22
|
// MARK: - Data Little-Endian Helpers (for BMP construction)
|
|
15
23
|
|
|
16
24
|
extension Data {
|
|
@@ -1524,6 +1532,8 @@ class G2: NSObject, SGCManager {
|
|
|
1524
1532
|
private var foregroundObserver: NSObjectProtocol?
|
|
1525
1533
|
private var startupPageCreated: Bool = false // createStartUpPageContainer can only be called once
|
|
1526
1534
|
private var pageCreated: Bool = false
|
|
1535
|
+
private var pageGeneration: UInt64 = 0
|
|
1536
|
+
private var lastImuReportTimestamp: Int64?
|
|
1527
1537
|
// Live hardware truth: is the firmware mic actually streaming. DISTINCT from the
|
|
1528
1538
|
// glasses/micEnabled DeviceStore flag, which is *intent* (does the user want the mic on).
|
|
1529
1539
|
// Cleared on every page teardown (the firmware kills the mic with the page) WITHOUT touching
|
|
@@ -2959,11 +2969,17 @@ class G2: NSObject, SGCManager {
|
|
|
2959
2969
|
}
|
|
2960
2970
|
signalDisplayDirty()
|
|
2961
2971
|
|
|
2962
|
-
try? await Task.sleep(nanoseconds: 300_000_000)
|
|
2972
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
|
|
2973
|
+
syncImuReportingWithIntent()
|
|
2974
|
+
confirmImuReporting(
|
|
2975
|
+
forPageGeneration: pageGeneration,
|
|
2976
|
+
commandSentAt: Int64(Date().timeIntervalSince1970 * 1000),
|
|
2977
|
+
attemptsRemaining: g2ImuMaxControlAttempts - 1
|
|
2978
|
+
)
|
|
2963
2979
|
restartMicIfAlreadyEnabled()
|
|
2964
2980
|
}
|
|
2965
2981
|
|
|
2966
|
-
/// Single coalesced recovery: rebuild the page + re-arm
|
|
2982
|
+
/// Single coalesced recovery: rebuild the page + re-arm sensor streams from intent, but never
|
|
2967
2983
|
/// stack rebuilds. The firmware spams systemExit/dashboard-close ~1×/sec; without this
|
|
2968
2984
|
/// guard each one triggered a rebuild that was torn down again → rebuild→exit→rebuild
|
|
2969
2985
|
/// storm. At most one rebuild in flight, and at most one per RECOVERY_DEBOUNCE_MS.
|
|
@@ -3481,6 +3497,8 @@ class G2: NSObject, SGCManager {
|
|
|
3481
3497
|
appId: activeMenuAppId
|
|
3482
3498
|
)
|
|
3483
3499
|
}
|
|
3500
|
+
pageGeneration &+= 1
|
|
3501
|
+
lastImuReportTimestamp = nil
|
|
3484
3502
|
sendEvenHubCommand(msg)
|
|
3485
3503
|
pageCreated = true
|
|
3486
3504
|
}
|
|
@@ -3492,6 +3510,62 @@ class G2: NSObject, SGCManager {
|
|
|
3492
3510
|
}
|
|
3493
3511
|
}
|
|
3494
3512
|
|
|
3513
|
+
/// Keep the firmware IMU stream alive at its slowest supported pace even when no miniapp
|
|
3514
|
+
/// requested motion data. Incoming keepalive samples are discarded in `handleTouchEvent`.
|
|
3515
|
+
private func syncImuReportingWithIntent(
|
|
3516
|
+
_ intentEnabled: Bool? = nil,
|
|
3517
|
+
requestedReportPace: Int32 = EvenHubProto.imuPaceP100
|
|
3518
|
+
) {
|
|
3519
|
+
guard pageCreated else { return }
|
|
3520
|
+
|
|
3521
|
+
let shouldForwardSamples =
|
|
3522
|
+
intentEnabled
|
|
3523
|
+
?? (DeviceStore.shared.get("bluetooth", "imu_enabled") as? Bool ?? false)
|
|
3524
|
+
let shouldStream = shouldForwardSamples || g2ImuKeepaliveEnabled
|
|
3525
|
+
let reportPace =
|
|
3526
|
+
shouldForwardSamples ? requestedReportPace : g2ImuKeepaliveReportPace
|
|
3527
|
+
|
|
3528
|
+
Bridge.log(
|
|
3529
|
+
"G2: syncing IMU stream — hardware=\(shouldStream), forward=\(shouldForwardSamples), pace=\(reportPace)"
|
|
3530
|
+
)
|
|
3531
|
+
let msg = EvenHubProto.imuControlMessage(
|
|
3532
|
+
enable: shouldStream,
|
|
3533
|
+
reportFrq: reportPace,
|
|
3534
|
+
magicRandom: sendManager.nextMagicRandom()
|
|
3535
|
+
)
|
|
3536
|
+
sendEvenHubCommand(msg)
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
/// EvenHub does not ACK IMU control commands, so use the data stream itself as confirmation.
|
|
3540
|
+
/// A page-generation guard prevents a delayed retry from targeting a replacement page.
|
|
3541
|
+
private func confirmImuReporting(
|
|
3542
|
+
forPageGeneration generation: UInt64,
|
|
3543
|
+
commandSentAt: Int64,
|
|
3544
|
+
attemptsRemaining: Int
|
|
3545
|
+
) {
|
|
3546
|
+
guard attemptsRemaining > 0 else { return }
|
|
3547
|
+
|
|
3548
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + g2ImuConfirmationDelay) { [weak self] in
|
|
3549
|
+
guard let self,
|
|
3550
|
+
self.pageCreated,
|
|
3551
|
+
self.pageGeneration == generation,
|
|
3552
|
+
g2ImuKeepaliveEnabled
|
|
3553
|
+
|| (DeviceStore.shared.get("bluetooth", "imu_enabled") as? Bool ?? false),
|
|
3554
|
+
!(self.lastImuReportTimestamp.map { $0 >= commandSentAt } ?? false)
|
|
3555
|
+
else { return }
|
|
3556
|
+
|
|
3557
|
+
Bridge.log(
|
|
3558
|
+
"G2: no IMU report received for page generation \(generation); retrying control (\(attemptsRemaining) remaining)"
|
|
3559
|
+
)
|
|
3560
|
+
self.syncImuReportingWithIntent()
|
|
3561
|
+
self.confirmImuReporting(
|
|
3562
|
+
forPageGeneration: generation,
|
|
3563
|
+
commandSentAt: Int64(Date().timeIntervalSince1970 * 1000),
|
|
3564
|
+
attemptsRemaining: attemptsRemaining - 1
|
|
3565
|
+
)
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3495
3569
|
func restartMic() {
|
|
3496
3570
|
// Intent is "mic on". The mic only exists inside a live EvenHub page, so we
|
|
3497
3571
|
// toggle it off then back on (the firmware needs the off→on edge to re-arm).
|
|
@@ -3829,21 +3903,7 @@ class G2: NSObject, SGCManager {
|
|
|
3829
3903
|
await rebuildState()
|
|
3830
3904
|
}
|
|
3831
3905
|
|
|
3832
|
-
|
|
3833
|
-
guard let self = self else { return }
|
|
3834
|
-
let msg = EvenHubProto.imuControlMessage(
|
|
3835
|
-
enable: enabled, reportFrq: reportFrq,
|
|
3836
|
-
magicRandom: self.sendManager.nextMagicRandom()
|
|
3837
|
-
)
|
|
3838
|
-
self.sendEvenHubCommand(msg)
|
|
3839
|
-
}
|
|
3840
|
-
|
|
3841
|
-
// If we just asked for a page, give it a moment to be created first.
|
|
3842
|
-
if enabled, !pageCreated {
|
|
3843
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: send)
|
|
3844
|
-
} else {
|
|
3845
|
-
send()
|
|
3846
|
-
}
|
|
3906
|
+
syncImuReportingWithIntent(enabled, requestedReportPace: reportFrq)
|
|
3847
3907
|
}
|
|
3848
3908
|
|
|
3849
3909
|
// MARK: - SGCManager: Device Control
|
|
@@ -4367,16 +4427,9 @@ class G2: NSObject, SGCManager {
|
|
|
4367
4427
|
return
|
|
4368
4428
|
}
|
|
4369
4429
|
|
|
4370
|
-
|
|
4371
4430
|
if cmdValue == EvenHubResponseCmd.osNotifyEventToApp.rawValue {
|
|
4372
|
-
//
|
|
4431
|
+
// Device event from glasses (touch/gesture or sensor data)
|
|
4373
4432
|
guard let devEventData = fields[13] as? Data else { return }
|
|
4374
|
-
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
4375
|
-
if lastClickTimestamp != nil && timestamp - lastClickTimestamp! < 100 {
|
|
4376
|
-
// Bridge.log("G2: Double click ignored (too soon)")
|
|
4377
|
-
return
|
|
4378
|
-
}
|
|
4379
|
-
lastClickTimestamp = timestamp
|
|
4380
4433
|
handleTouchEvent(devEventData)
|
|
4381
4434
|
} else if cmdValue == 17 {
|
|
4382
4435
|
// Miniapp selection from glasses dashboard menu (cmdId=17)
|
|
@@ -4526,6 +4579,35 @@ class G2: NSObject, SGCManager {
|
|
|
4526
4579
|
|
|
4527
4580
|
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
4528
4581
|
|
|
4582
|
+
// Classify IMU reports before applying the click debounce. Keepalive samples arrive
|
|
4583
|
+
// continuously and must not update `lastClickTimestamp`, or a real gesture delivered
|
|
4584
|
+
// within 100 ms of a sample can be dropped.
|
|
4585
|
+
var parsedSysFields: [Int: Any]?
|
|
4586
|
+
if let sysData = fields[3] as? Data {
|
|
4587
|
+
var sysReader = ProtobufReader(sysData)
|
|
4588
|
+
let sysFields = sysReader.parseFields()
|
|
4589
|
+
if (sysFields[1] as? Int32) == OsEventType.imuDataReport.rawValue {
|
|
4590
|
+
lastImuReportTimestamp = timestamp
|
|
4591
|
+
let shouldForwardSamples =
|
|
4592
|
+
DeviceStore.shared.get("bluetooth", "imu_enabled") as? Bool ?? false
|
|
4593
|
+
if shouldForwardSamples,
|
|
4594
|
+
let imuData = sysFields[3] as? Data,
|
|
4595
|
+
let imu = parseImuReportData(imuData)
|
|
4596
|
+
{
|
|
4597
|
+
Bridge.log("G2: IMU data report: \(imu.x), \(imu.y), \(imu.z)")
|
|
4598
|
+
Bridge.sendAccelEvent(x: imu.x, y: imu.y, z: imu.z, timestamp: timestamp)
|
|
4599
|
+
}
|
|
4600
|
+
return
|
|
4601
|
+
}
|
|
4602
|
+
parsedSysFields = sysFields
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4605
|
+
if lastClickTimestamp != nil && timestamp - lastClickTimestamp! < 100 {
|
|
4606
|
+
// Bridge.log("G2: Double click ignored (too soon)")
|
|
4607
|
+
return
|
|
4608
|
+
}
|
|
4609
|
+
lastClickTimestamp = timestamp
|
|
4610
|
+
|
|
4529
4611
|
// if we are receiving touch events we are fully booted:
|
|
4530
4612
|
setFullyConnected()
|
|
4531
4613
|
|
|
@@ -4534,22 +4616,7 @@ class G2: NSObject, SGCManager {
|
|
|
4534
4616
|
// "G2: handleTouchEvent: \(devEventData.map { String(format: "%02X", $0) }.joined())")
|
|
4535
4617
|
|
|
4536
4618
|
// SysEvent (field 3) - system-level gestures
|
|
4537
|
-
if let
|
|
4538
|
-
var sysReader = ProtobufReader(sysData)
|
|
4539
|
-
let sysFields = sysReader.parseFields()
|
|
4540
|
-
|
|
4541
|
-
// IMU data report: eventType == IMU_DATA_REPORT (8), imuData in field 3
|
|
4542
|
-
// (IMU_Report_Data { x, y, z } as 32-bit floats). Handle and return before
|
|
4543
|
-
// the gesture-mapping path.
|
|
4544
|
-
if (sysFields[1] as? Int32) == OsEventType.imuDataReport.rawValue,
|
|
4545
|
-
let imuData = sysFields[3] as? Data,
|
|
4546
|
-
let imu = parseImuReportData(imuData)
|
|
4547
|
-
{
|
|
4548
|
-
Bridge.log("G2: IMU data report: \(imu.x), \(imu.y), \(imu.z)")
|
|
4549
|
-
Bridge.sendAccelEvent(x: imu.x, y: imu.y, z: imu.z, timestamp: timestamp)
|
|
4550
|
-
return
|
|
4551
|
-
}
|
|
4552
|
-
|
|
4619
|
+
if let sysFields = parsedSysFields {
|
|
4553
4620
|
var eventType: OsEventType? = nil
|
|
4554
4621
|
var eventSource: Int32? = nil
|
|
4555
4622
|
if let normalType = sysFields[1] as? Int32 {
|
|
@@ -1282,13 +1282,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1282
1282
|
/// Mirrors Android `updateConnectionState` — RN home reads `glasses.connectionState` for reconnecting UI.
|
|
1283
1283
|
private func updateConnectionState(_ state: String) {
|
|
1284
1284
|
if state == ConnTypes.DISCONNECTED {
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
//
|
|
1288
|
-
// clearing on CONNECTED would wipe
|
|
1289
|
-
// glasses_ready (e.g. ASG restart) re-publishes CONNECTED (iOS has no equal-state
|
|
1290
|
-
//
|
|
1285
|
+
// Device identity is session-bound. Clear it on disconnect so a previous pair's
|
|
1286
|
+
// identifiers cannot be associated with the next connection. Connect must NOT clear
|
|
1287
|
+
// them: DeviceManager.disconnect already wipes them before any new connection, and
|
|
1288
|
+
// clearing on CONNECTED would wipe still-valid identity mid-session when a same-link
|
|
1289
|
+
// glasses_ready (e.g. ASG restart) re-publishes CONNECTED (iOS has no equal-state early
|
|
1290
|
+
// return, unlike Android).
|
|
1291
1291
|
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
1292
|
+
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", "")
|
|
1292
1293
|
}
|
|
1293
1294
|
connectionState = state
|
|
1294
1295
|
DeviceStore.shared.apply("glasses", "connectionState", state)
|
|
@@ -2760,10 +2761,10 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2760
2761
|
if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
|
|
2761
2762
|
DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
|
|
2762
2763
|
}
|
|
2763
|
-
if let bluetoothMacAddress = fields
|
|
2764
|
+
if let bluetoothMacAddress = nonEmptyStringValue(fields, "bt_mac_address") {
|
|
2764
2765
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
2765
2766
|
}
|
|
2766
|
-
if let serialNumber = fields
|
|
2767
|
+
if let serialNumber = nonEmptyStringValue(fields, "serial_number") {
|
|
2767
2768
|
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
2768
2769
|
}
|
|
2769
2770
|
if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
|
|
@@ -3441,15 +3442,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3441
3442
|
let androidVersion = json["android_version"] as? String ?? ""
|
|
3442
3443
|
let otaVersionUrl = json["ota_version_url"] as? String ?? ""
|
|
3443
3444
|
let firmwareVersion = json["firmware_version"] as? String ?? ""
|
|
3444
|
-
let bluetoothMacAddress = json
|
|
3445
|
-
let serialNumber = json
|
|
3445
|
+
let bluetoothMacAddress = nonEmptyStringValue(json, "bt_mac_address")
|
|
3446
|
+
let serialNumber = nonEmptyStringValue(json, "serial_number")
|
|
3446
3447
|
|
|
3447
3448
|
DeviceStore.shared.apply("glasses", "appVersion", appVersion)
|
|
3448
3449
|
DeviceStore.shared.apply("glasses", "buildNumber", buildNumber)
|
|
3449
3450
|
DeviceStore.shared.apply("glasses", "otaVersionUrl", otaVersionUrl)
|
|
3450
3451
|
DeviceStore.shared.apply("glasses", "firmwareVersion", firmwareVersion)
|
|
3451
|
-
|
|
3452
|
-
|
|
3452
|
+
if let bluetoothMacAddress {
|
|
3453
|
+
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
3454
|
+
}
|
|
3455
|
+
if let serialNumber {
|
|
3453
3456
|
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
3454
3457
|
}
|
|
3455
3458
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
@@ -3462,14 +3465,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3462
3465
|
// hasMic = supportsLC3Audio
|
|
3463
3466
|
|
|
3464
3467
|
Bridge.log(
|
|
3465
|
-
"Glasses Version - App: \(appVersion), Build: \(buildNumber), Device: \(deviceModel), Android: \(androidVersion), Firmware: \(firmwareVersion), BT MAC: \(bluetoothMacAddress), OTA URL: \(otaVersionUrl)"
|
|
3468
|
+
"Glasses Version - App: \(appVersion), Build: \(buildNumber), Device: \(deviceModel), Android: \(androidVersion), Firmware: \(firmwareVersion), BT MAC available: \(bluetoothMacAddress != nil), OTA URL: \(otaVersionUrl)"
|
|
3466
3469
|
)
|
|
3467
3470
|
Bridge.log("LIVE: LC3 Audio Support: \(supportsLC3Audio), Has Mic: \(hasMic)")
|
|
3468
3471
|
emitVersionInfo(
|
|
3469
3472
|
appVersion: appVersion, buildNumber: buildNumber, deviceModel: deviceModel,
|
|
3470
3473
|
androidVersion: androidVersion, otaVersionUrl: otaVersionUrl,
|
|
3471
3474
|
firmwareVersion: firmwareVersion,
|
|
3472
|
-
bluetoothMacAddress: bluetoothMacAddress
|
|
3475
|
+
bluetoothMacAddress: bluetoothMacAddress ?? ""
|
|
3473
3476
|
)
|
|
3474
3477
|
}
|
|
3475
3478
|
|
|
@@ -274,19 +274,22 @@ public struct StreamRequest {
|
|
|
274
274
|
public let sound: Bool
|
|
275
275
|
public let video: StreamVideoConfig?
|
|
276
276
|
public let audio: StreamAudioConfig?
|
|
277
|
+
public let authToken: String?
|
|
277
278
|
|
|
278
279
|
public init(
|
|
279
280
|
streamUrl: String,
|
|
280
281
|
streamId: String = "",
|
|
281
282
|
sound: Bool = true,
|
|
282
283
|
video: StreamVideoConfig? = nil,
|
|
283
|
-
audio: StreamAudioConfig? = nil
|
|
284
|
+
audio: StreamAudioConfig? = nil,
|
|
285
|
+
authToken: String? = nil
|
|
284
286
|
) {
|
|
285
287
|
self.streamUrl = streamUrl
|
|
286
288
|
self.streamId = streamId
|
|
287
289
|
self.sound = sound
|
|
288
290
|
self.video = video
|
|
289
291
|
self.audio = audio
|
|
292
|
+
self.authToken = authToken
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
init(values: [String: Any]) {
|
|
@@ -299,7 +302,8 @@ public struct StreamRequest {
|
|
|
299
302
|
streamId: values["streamId"] as? String ?? "",
|
|
300
303
|
sound: values["sound"] as? Bool ?? true,
|
|
301
304
|
video: StreamVideoConfig(values: values["video"] as? [String: Any]),
|
|
302
|
-
audio: StreamAudioConfig(values: values["audio"] as? [String: Any])
|
|
305
|
+
audio: StreamAudioConfig(values: values["audio"] as? [String: Any]),
|
|
306
|
+
authToken: values["authToken"] as? String ?? values["auth_token"] as? String
|
|
303
307
|
)
|
|
304
308
|
}
|
|
305
309
|
|
|
@@ -315,6 +319,9 @@ public struct StreamRequest {
|
|
|
315
319
|
if let audioValues = audio?.dictionary, !audioValues.isEmpty {
|
|
316
320
|
values["audio"] = audioValues
|
|
317
321
|
}
|
|
322
|
+
if let authToken, !authToken.isEmpty {
|
|
323
|
+
values["authToken"] = authToken
|
|
324
|
+
}
|
|
318
325
|
return values
|
|
319
326
|
}
|
|
320
327
|
}
|