@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-dev.12

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.
Files changed (118) hide show
  1. package/README.md +56 -9
  2. package/android/build.gradle +7 -4
  3. package/android/lc3Lib/build.gradle +18 -4
  4. package/android/silero/build.gradle +9 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +35 -4
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +205 -19
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
  12. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
  13. package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
  20. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1137 -191
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  27. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  28. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  29. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +75 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  35. package/build/BluetoothSdk.types.d.ts +56 -2
  36. package/build/BluetoothSdk.types.d.ts.map +1 -1
  37. package/build/BluetoothSdk.types.js.map +1 -1
  38. package/build/_internal.d.ts +2 -0
  39. package/build/_internal.d.ts.map +1 -1
  40. package/build/_internal.js +2 -0
  41. package/build/_internal.js.map +1 -1
  42. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  43. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  44. package/build/_private/BluetoothSdkModule.js +1 -1
  45. package/build/_private/BluetoothSdkModule.js.map +1 -1
  46. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  47. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  48. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  49. package/build/_private/sdkOtaManifest.d.ts +3 -12
  50. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  51. package/build/_private/sdkOtaManifest.js +5 -17
  52. package/build/_private/sdkOtaManifest.js.map +1 -1
  53. package/build/generated/releaseMetadata.d.ts +12 -0
  54. package/build/generated/releaseMetadata.d.ts.map +1 -0
  55. package/build/generated/releaseMetadata.js +10 -0
  56. package/build/generated/releaseMetadata.js.map +1 -0
  57. package/build/index.d.ts +1 -1
  58. package/build/index.d.ts.map +1 -1
  59. package/build/index.js +10 -2
  60. package/build/index.js.map +1 -1
  61. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  62. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  63. package/build/ota-server/MentraOtaServer.types.js +2 -0
  64. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  65. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  66. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  67. package/build/ota-server/MentraOtaServerModule.js +3 -0
  68. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  69. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  70. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  71. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  72. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  73. package/build/ota-server/index.d.ts +3 -0
  74. package/build/ota-server/index.d.ts.map +1 -0
  75. package/build/ota-server/index.js +3 -0
  76. package/build/ota-server/index.js.map +1 -0
  77. package/expo-module.config.json +3 -2
  78. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  79. package/ios/BluetoothSdkModule.swift +16 -2
  80. package/ios/LocalIPv4.swift +150 -0
  81. package/ios/LocalOtaServer.swift +309 -0
  82. package/ios/MentraBluetoothSDK.podspec +5 -1
  83. package/ios/MentraOtaServerModule.swift +147 -0
  84. package/ios/MentraPhotoReceiverModule.swift +1 -81
  85. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  86. package/ios/Source/Bridge.swift +43 -4
  87. package/ios/Source/DeviceManager.swift +143 -13
  88. package/ios/Source/DeviceStore.swift +2 -0
  89. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  90. package/ios/Source/MentraBluetoothSDK.swift +74 -6
  91. package/ios/Source/OtaManifest.swift +5 -7
  92. package/ios/Source/audio/AudioModels.swift +17 -0
  93. package/ios/Source/camera/CameraModels.swift +1 -0
  94. package/ios/Source/events/BluetoothEvents.swift +27 -0
  95. package/ios/Source/sgcs/MentraLive.swift +410 -30
  96. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  97. package/ios/Source/sgcs/SGCManager.swift +11 -0
  98. package/ios/Source/status/DeviceStatus.swift +23 -0
  99. package/ios/Source/types/DeviceModels.swift +23 -2
  100. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  101. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +104 -0
  102. package/package.json +5 -1
  103. package/plugin/build/withIos.d.ts +3 -1
  104. package/plugin/build/withIos.js +22 -10
  105. package/scripts/verify-release-package.mjs +34 -0
  106. package/scripts/write-release-metadata.mjs +154 -0
  107. package/scripts/write-release-metadata.test.mjs +29 -0
  108. package/src/BluetoothSdk.types.ts +67 -3
  109. package/src/_internal.ts +2 -0
  110. package/src/_private/BluetoothSdkModule.ts +5 -5
  111. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  112. package/src/_private/sdkOtaManifest.ts +5 -17
  113. package/src/generated/releaseMetadata.ts +20 -0
  114. package/src/index.ts +19 -2
  115. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  116. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  117. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  118. package/src/ota-server/index.ts +2 -0
@@ -309,6 +309,8 @@ public final class MentraBluetoothSDK {
309
309
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", device.name)
310
310
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", device.identifier ?? "")
311
311
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", device.projectName ?? "")
312
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
313
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
312
314
  finishDefaultDeviceApply(generation: generation)
313
315
  }
314
316
 
@@ -320,6 +322,8 @@ public final class MentraBluetoothSDK {
320
322
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", "")
321
323
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", "")
322
324
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", "")
325
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
326
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
323
327
  finishDefaultDeviceApply(generation: generation)
324
328
  }
325
329
 
@@ -394,6 +398,9 @@ public final class MentraBluetoothSDK {
394
398
  }
395
399
  if options.saveAsDefault && !isController {
396
400
  setDefaultDevice(device)
401
+ } else if !isController {
402
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", device.name)
403
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", device.identifier ?? "")
397
404
  }
398
405
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_wearable", device.model.deviceType)
399
406
  DeviceManager.shared.connectByName(device.name)
@@ -418,6 +425,8 @@ public final class MentraBluetoothSDK {
418
425
 
419
426
  public func cancelConnectionAttempt() {
420
427
  clearBluetoothRestoreIntent()
428
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
429
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
421
430
  DeviceManager.shared.disconnect()
422
431
  }
423
432
 
@@ -428,6 +437,8 @@ public final class MentraBluetoothSDK {
428
437
 
429
438
  public func disconnect() {
430
439
  clearBluetoothRestoreIntent()
440
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
441
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
431
442
  DeviceManager.shared.disconnect()
432
443
  }
433
444
 
@@ -890,6 +901,13 @@ public final class MentraBluetoothSDK {
890
901
  }
891
902
  }
892
903
 
904
+ /// Fire-and-forget Mentra Live Wi-Fi ADB toggle. Glasses do not ack this
905
+ /// command, so there is no pending-response wait (unlike hotspot / Wi-Fi forget).
906
+ public func setWifiAdbState(enabled: Bool) async throws {
907
+ try requireGlassesConnected(operation: "toggle Wi-Fi ADB")
908
+ DeviceManager.shared.setWifiAdbState(enabled)
909
+ }
910
+
893
911
  func setSystemTime(timestampMs: Int64) {
894
912
  DeviceManager.shared.setSystemTime(timestampMs)
895
913
  }
@@ -1141,6 +1159,36 @@ public final class MentraBluetoothSDK {
1141
1159
  }
1142
1160
  }
1143
1161
 
1162
+ public func queryVideoRecordingStatus(requestId: String) async throws -> VideoRecordingStatusEvent {
1163
+ guard !requestId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
1164
+ throw BluetoothSdkError(
1165
+ code: "missing_request_id",
1166
+ message: "requestId is required to query video recording status."
1167
+ )
1168
+ }
1169
+ try requireGlassesConnected(operation: "query video recording status")
1170
+ let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "query video recording status \(requestId)")
1171
+ guard pendingVideoRecordingRequests[requestId] == nil else {
1172
+ throw BluetoothSdkError(
1173
+ code: "request_in_flight",
1174
+ message: "A video recording command is already waiting for requestId \(requestId)."
1175
+ )
1176
+ }
1177
+ pendingVideoRecordingRequests[requestId] = PendingVideoRecordingRequest(
1178
+ expectedStatus: "recording_status",
1179
+ pending: pending
1180
+ )
1181
+ DeviceManager.shared.queryVideoRecordingStatus(requestId)
1182
+ do {
1183
+ let event = try await pending.wait()
1184
+ pendingVideoRecordingRequests.removeValue(forKey: requestId)
1185
+ return event
1186
+ } catch {
1187
+ pendingVideoRecordingRequests.removeValue(forKey: requestId)
1188
+ throw error
1189
+ }
1190
+ }
1191
+
1144
1192
  public func requestVersionInfo() async throws -> VersionInfoResult {
1145
1193
  guard pendingVersionInfo == nil else {
1146
1194
  throw BluetoothSdkError(
@@ -1165,11 +1213,13 @@ public final class MentraBluetoothSDK {
1165
1213
  }
1166
1214
  }
1167
1215
 
1168
- func setOtaVersionUrl(_ otaVersionUrl: String) throws {
1216
+ /// Select the HTTP(S) manifest used by subsequent OTA checks and installs.
1217
+ public func setOtaVersionUrl(_ otaVersionUrl: String) throws {
1169
1218
  configuredOtaVersionUrl = try OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
1170
1219
  }
1171
1220
 
1172
- func getOtaVersionUrl() throws -> String {
1221
+ /// Return the configured OTA manifest, or this SDK release's default manifest.
1222
+ public func getOtaVersionUrl() throws -> String {
1173
1223
  try configuredOtaVersionUrl ?? OtaManifestDefaults.defaultOtaVersionUrl()
1174
1224
  }
1175
1225
 
@@ -2057,6 +2107,8 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2057
2107
  if !event.lc3.isEmpty {
2058
2108
  delegate?.mentraBluetoothSDK(self, didReceiveMicLc3: event)
2059
2109
  }
2110
+ case "mic_health":
2111
+ delegate?.mentraBluetoothSDK(self, didReceive: .micHealth(MicHealthEvent(values: data)))
2060
2112
  case "local_transcription":
2061
2113
  let event = LocalTranscriptionEvent(
2062
2114
  text: data["text"] as? String ?? "",
@@ -2105,6 +2157,10 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2105
2157
  let event = GalleryStatusEvent(values: data)
2106
2158
  pendingGalleryStatus?.resolve(event)
2107
2159
  delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "gallery_status", values: event.values))
2160
+ case "pairing_info":
2161
+ delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "pairing_info", values: data))
2162
+ case "entering_pairing_mode", "owner_replaced":
2163
+ delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
2108
2164
  case "photo_response":
2109
2165
  let event = PhotoResponseEvent(values: data)
2110
2166
  handlePhotoResponseForRequests(event)
@@ -2147,7 +2203,13 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2147
2203
  var resultValues = data
2148
2204
  resultValues["type"] = "ota_status"
2149
2205
  pendingOtaQuery?.resolve(OtaQueryResult(values: resultValues))
2150
- delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(OtaStatusEvent(values: resultValues)))
2206
+ let event = OtaStatusEvent(values: resultValues)
2207
+ if let errorCode = otaStartRejectionErrorCode(event) {
2208
+ pendingOtaStart?.reject(
2209
+ BluetoothSdkError(code: errorCode, message: "Glasses rejected OTA start: \(errorCode)")
2210
+ )
2211
+ }
2212
+ delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(event))
2151
2213
  case "settings_ack":
2152
2214
  let event = SettingsAckEvent(values: data)
2153
2215
  handleSettingsAckForRequests(event)
@@ -2172,6 +2234,12 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2172
2234
  }
2173
2235
  }
2174
2236
 
2175
-
2176
-
2177
-
2237
+ /// OTA status messages are not request-correlated, so only known pre-ack failures settle a start.
2238
+ private func otaStartRejectionErrorCode(_ event: OtaStatusEvent) -> String? {
2239
+ guard event.status.lowercased() == "failed",
2240
+ event.errorMessage?.lowercased() == "battery_low"
2241
+ else {
2242
+ return nil
2243
+ }
2244
+ return "battery_low"
2245
+ }
@@ -1,21 +1,19 @@
1
1
  import Foundation
2
2
 
3
3
  enum OtaManifestDefaults {
4
- private static let sdkOtaReleaseBaseUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota"
5
4
  // ASG builds before 39 ignore ota_start.ota_version_url, so SDK checks must
6
5
  // use the same legacy production manifest those glasses will install from.
7
6
  static let legacyProdOtaVersionUrl = "https://ota.mentraglass.com/prod_live_version.json"
8
7
 
9
8
  static func defaultOtaVersionUrl() throws -> String {
10
- guard let sdkVersion = BluetoothSdkDefaults.sdkVersion,
11
- !sdkVersion.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
12
- else {
9
+ let manifestUrl = GeneratedReleaseMetadata.otaManifestUrl.trimmingCharacters(in: .whitespacesAndNewlines)
10
+ guard !manifestUrl.isEmpty else {
13
11
  throw BluetoothSdkError(
14
- code: "missing_sdk_version",
15
- message: "Cannot determine Bluetooth SDK version for the default OTA manifest URL."
12
+ code: "ota_manifest_unconfigured",
13
+ message: "This source-built Bluetooth SDK has no embedded OTA manifest. Configure one with the debug API."
16
14
  )
17
15
  }
18
- return "\(sdkOtaReleaseBaseUrl)/bluetooth-sdk-\(sdkVersion)-version.json"
16
+ return manifestUrl
19
17
  }
20
18
  }
21
19
 
@@ -7,6 +7,23 @@ public enum MicPreference: String {
7
7
  case bluetooth
8
8
  }
9
9
 
10
+ struct MicHealth {
11
+ let sequenceGapEvents: Int64
12
+ let decodeFailures: Int64
13
+ let lastLc3ReceivedAt: Int64?
14
+ let lastPcmProducedAt: Int64?
15
+
16
+ var dictionary: [String: Any] {
17
+ var values: [String: Any] = [
18
+ "sequenceGapEvents": sequenceGapEvents,
19
+ "decodeFailures": decodeFailures,
20
+ ]
21
+ if let lastLc3ReceivedAt { values["lastLc3ReceivedAt"] = lastLc3ReceivedAt }
22
+ if let lastPcmProducedAt { values["lastPcmProducedAt"] = lastPcmProducedAt }
23
+ return values
24
+ }
25
+ }
26
+
10
27
  public struct LocalTranscriptionEvent: CustomStringConvertible {
11
28
  public let text: String
12
29
  public let isFinal: Bool
@@ -863,3 +863,4 @@ public struct GalleryStatusEvent: CustomStringConvertible {
863
863
  "GalleryStatusEvent(total: \(total), photos: \(photos), videos: \(videos))"
864
864
  }
865
865
  }
866
+
@@ -73,6 +73,30 @@ public struct SpeakingStatusEvent: CustomStringConvertible {
73
73
  }
74
74
  }
75
75
 
76
+ public struct MicHealthEvent: CustomStringConvertible {
77
+ public let reason: String
78
+ public let sequenceGapEvents: Int
79
+ public let decodeFailures: Int
80
+ public let lastLc3ReceivedAt: Int?
81
+ public let lastPcmProducedAt: Int?
82
+ public let timestamp: Int
83
+ public let values: [String: Any]
84
+
85
+ public init(values: [String: Any]) {
86
+ reason = stringValue(values, "reason") ?? ""
87
+ sequenceGapEvents = intValue(values["sequenceGapEvents"]) ?? 0
88
+ decodeFailures = intValue(values["decodeFailures"]) ?? 0
89
+ lastLc3ReceivedAt = intValue(values["lastLc3ReceivedAt"])
90
+ lastPcmProducedAt = intValue(values["lastPcmProducedAt"])
91
+ timestamp = intValue(values["timestamp"]) ?? Int(Date().timeIntervalSince1970 * 1000)
92
+ self.values = values
93
+ }
94
+
95
+ public var description: String {
96
+ "MicHealthEvent(reason: \(reason), sequenceGapEvents: \(sequenceGapEvents), decodeFailures: \(decodeFailures))"
97
+ }
98
+ }
99
+
76
100
  public struct OtaStartAckEvent: CustomStringConvertible {
77
101
  public let timestamp: Int?
78
102
  public let values: [String: Any]
@@ -216,6 +240,7 @@ public enum BluetoothEvent: CustomStringConvertible {
216
240
  case touch(TouchEvent)
217
241
  case voiceActivityDetectionStatus(VoiceActivityDetectionStatusEvent)
218
242
  case speakingStatus(SpeakingStatusEvent)
243
+ case micHealth(MicHealthEvent)
219
244
  case wifiStatus(WifiStatusEvent)
220
245
  case hotspotStatus(HotspotStatusEvent)
221
246
  case hotspotError(HotspotErrorEvent)
@@ -244,6 +269,8 @@ public enum BluetoothEvent: CustomStringConvertible {
244
269
  event.description
245
270
  case let .speakingStatus(event):
246
271
  event.description
272
+ case let .micHealth(event):
273
+ event.description
247
274
  case let .wifiStatus(event):
248
275
  event.description
249
276
  case let .hotspotStatus(event):