@mentra/bluetooth-sdk 0.1.21-beta.2 → 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 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, so they emit this event as soon as this SDK version ships. Mentra Live instead reports the serial provisioned in BES NV storage, which requires compatible BES firmware and `asg_client` software.
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
- // A manufacturing serial is session-bound. Clear it on every disconnect so a
2054
- // previously connected pair's serial can never be reported for the next
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
- // A manufacturing serial is session-bound. Clear it on disconnect so a previous
821
- // pair's serial can never be associated with the next connection. Connect must NOT
822
- // clear it: DeviceManager.disconnect already wipes it before any new connection, and
823
- // clearing on CONNECTED would wipe a still-valid serial mid-session when a same-link
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
- DeviceStore.apply("glasses", "bluetoothMacAddress", btMacAddressLegacy)
3733
- if (serialNumberLegacy.isNotBlank()) {
3734
- DeviceStore.apply("glasses", "serialNumber", serialNumberLegacy)
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
- if (fields.containsKey("bt_mac_address")) {
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
- val serialNumber = fields["serial_number"] as? String
4005
- if (!serialNumber.isNullOrBlank()) {
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"]
@@ -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.2"
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
- // A manufacturing serial is session-bound. Clear it on every disconnect so a
1716
- // previously connected pair's serial can never be reported for the next
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", "")
@@ -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) // 300ms to settle
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 the mic from intent, but never
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
- let send = { [weak self] in
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
- // Touch/gesture event from glasses
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 sysData = fields[3] as? Data {
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
- // A manufacturing serial is session-bound. Clear it on disconnect so a previous
1286
- // pair's serial cannot be associated with the next connection. Connect must NOT
1287
- // clear it: DeviceManager.disconnect already wipes it before any new connection, and
1288
- // clearing on CONNECTED would wipe a still-valid serial mid-session when a same-link
1289
- // glasses_ready (e.g. ASG restart) re-publishes CONNECTED (iOS has no equal-state
1290
- // early return, unlike Android).
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["bt_mac_address"] as? String {
2764
+ if let bluetoothMacAddress = nonEmptyStringValue(fields, "bt_mac_address") {
2764
2765
  DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
2765
2766
  }
2766
- if let serialNumber = fields["serial_number"] as? String, !serialNumber.isEmpty {
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["bt_mac_address"] as? String ?? ""
3445
- let serialNumber = json["serial_number"] as? String ?? ""
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
- DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
3452
- if !serialNumber.isEmpty {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.21-beta.2",
3
+ "version": "0.1.21-beta.3",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",