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

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 (140) hide show
  1. package/README.md +67 -25
  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/GeneratedChangelogCatalog.kt +7 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +127 -8
  12. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
  13. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
  14. package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
  22. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1186 -191
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  29. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  30. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
  37. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  38. package/build/BluetoothSdk.types.d.ts +82 -2
  39. package/build/BluetoothSdk.types.d.ts.map +1 -1
  40. package/build/BluetoothSdk.types.js.map +1 -1
  41. package/build/_internal.d.ts +2 -0
  42. package/build/_internal.d.ts.map +1 -1
  43. package/build/_internal.js +2 -0
  44. package/build/_internal.js.map +1 -1
  45. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  46. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  47. package/build/_private/BluetoothSdkModule.js +1 -1
  48. package/build/_private/BluetoothSdkModule.js.map +1 -1
  49. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  50. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  51. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  52. package/build/_private/sdkOtaManifest.d.ts +3 -12
  53. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  54. package/build/_private/sdkOtaManifest.js +5 -17
  55. package/build/_private/sdkOtaManifest.js.map +1 -1
  56. package/build/changelogs.d.ts +7 -0
  57. package/build/changelogs.d.ts.map +1 -0
  58. package/build/changelogs.js +44 -0
  59. package/build/changelogs.js.map +1 -0
  60. package/build/generated/changelogCatalog.d.ts +6 -0
  61. package/build/generated/changelogCatalog.d.ts.map +1 -0
  62. package/build/generated/changelogCatalog.js +8 -0
  63. package/build/generated/changelogCatalog.js.map +1 -0
  64. package/build/generated/releaseMetadata.d.ts +12 -0
  65. package/build/generated/releaseMetadata.d.ts.map +1 -0
  66. package/build/generated/releaseMetadata.js +10 -0
  67. package/build/generated/releaseMetadata.js.map +1 -0
  68. package/build/index.d.ts +1 -1
  69. package/build/index.d.ts.map +1 -1
  70. package/build/index.js +24 -2
  71. package/build/index.js.map +1 -1
  72. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  73. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  74. package/build/ota-server/MentraOtaServer.types.js +2 -0
  75. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  76. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  77. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  78. package/build/ota-server/MentraOtaServerModule.js +3 -0
  79. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  80. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  81. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  82. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  83. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  84. package/build/ota-server/index.d.ts +3 -0
  85. package/build/ota-server/index.d.ts.map +1 -0
  86. package/build/ota-server/index.js +3 -0
  87. package/build/ota-server/index.js.map +1 -0
  88. package/build/ota-transport/index.d.ts +67 -0
  89. package/build/ota-transport/index.d.ts.map +1 -0
  90. package/build/ota-transport/index.js +58 -0
  91. package/build/ota-transport/index.js.map +1 -0
  92. package/expo-module.config.json +3 -2
  93. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  94. package/ios/BluetoothSdkModule.swift +16 -2
  95. package/ios/LocalIPv4.swift +150 -0
  96. package/ios/LocalOtaServer.swift +309 -0
  97. package/ios/MentraBluetoothSDK.podspec +5 -1
  98. package/ios/MentraOtaServerModule.swift +147 -0
  99. package/ios/MentraPhotoReceiverModule.swift +1 -81
  100. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  101. package/ios/Source/Bridge.swift +43 -4
  102. package/ios/Source/DeviceManager.swift +143 -13
  103. package/ios/Source/DeviceStore.swift +2 -0
  104. package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
  105. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  106. package/ios/Source/MentraBluetoothSDK.swift +91 -6
  107. package/ios/Source/OtaManifest.swift +5 -7
  108. package/ios/Source/ReleaseChangelog.swift +65 -0
  109. package/ios/Source/audio/AudioModels.swift +17 -0
  110. package/ios/Source/camera/CameraModels.swift +1 -0
  111. package/ios/Source/events/BluetoothEvents.swift +27 -0
  112. package/ios/Source/sgcs/MentraLive.swift +453 -31
  113. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  114. package/ios/Source/sgcs/SGCManager.swift +11 -0
  115. package/ios/Source/status/DeviceStatus.swift +23 -0
  116. package/ios/Source/types/DeviceModels.swift +23 -2
  117. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  118. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
  119. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  120. package/package.json +10 -1
  121. package/plugin/build/withIos.d.ts +3 -1
  122. package/plugin/build/withIos.js +22 -10
  123. package/scripts/public-ota-api.test.mjs +37 -0
  124. package/scripts/verify-release-package.mjs +34 -0
  125. package/scripts/write-release-metadata.mjs +154 -0
  126. package/scripts/write-release-metadata.test.mjs +29 -0
  127. package/src/BluetoothSdk.types.ts +95 -3
  128. package/src/_internal.ts +2 -0
  129. package/src/_private/BluetoothSdkModule.ts +5 -5
  130. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  131. package/src/_private/sdkOtaManifest.ts +5 -17
  132. package/src/changelogs.ts +44 -0
  133. package/src/generated/changelogCatalog.ts +9 -0
  134. package/src/generated/releaseMetadata.ts +20 -0
  135. package/src/index.ts +43 -2
  136. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  137. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  138. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  139. package/src/ota-server/index.ts +2 -0
  140. package/src/ota-transport/index.ts +116 -0
@@ -309,6 +309,9 @@ 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", "")
314
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
312
315
  finishDefaultDeviceApply(generation: generation)
313
316
  }
314
317
 
@@ -320,6 +323,9 @@ public final class MentraBluetoothSDK {
320
323
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", "")
321
324
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", "")
322
325
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", "")
326
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
327
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
328
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
323
329
  finishDefaultDeviceApply(generation: generation)
324
330
  }
325
331
 
@@ -394,6 +400,14 @@ public final class MentraBluetoothSDK {
394
400
  }
395
401
  if options.saveAsDefault && !isController {
396
402
  setDefaultDevice(device)
403
+ } else if !isController {
404
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", device.name)
405
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", device.identifier ?? "")
406
+ DeviceStore.shared.apply(
407
+ ObservableStore.bluetoothCategory,
408
+ "pending_device_secure_pairing_capable",
409
+ device.securePairingCapable ?? ""
410
+ )
397
411
  }
398
412
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_wearable", device.model.deviceType)
399
413
  DeviceManager.shared.connectByName(device.name)
@@ -418,6 +432,9 @@ public final class MentraBluetoothSDK {
418
432
 
419
433
  public func cancelConnectionAttempt() {
420
434
  clearBluetoothRestoreIntent()
435
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
436
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
437
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
421
438
  DeviceManager.shared.disconnect()
422
439
  }
423
440
 
@@ -428,6 +445,9 @@ public final class MentraBluetoothSDK {
428
445
 
429
446
  public func disconnect() {
430
447
  clearBluetoothRestoreIntent()
448
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
449
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
450
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
431
451
  DeviceManager.shared.disconnect()
432
452
  }
433
453
 
@@ -890,6 +910,13 @@ public final class MentraBluetoothSDK {
890
910
  }
891
911
  }
892
912
 
913
+ /// Fire-and-forget Mentra Live Wi-Fi ADB toggle. Glasses do not ack this
914
+ /// command, so there is no pending-response wait (unlike hotspot / Wi-Fi forget).
915
+ public func setWifiAdbState(enabled: Bool) async throws {
916
+ try requireGlassesConnected(operation: "toggle Wi-Fi ADB")
917
+ DeviceManager.shared.setWifiAdbState(enabled)
918
+ }
919
+
893
920
  func setSystemTime(timestampMs: Int64) {
894
921
  DeviceManager.shared.setSystemTime(timestampMs)
895
922
  }
@@ -1141,6 +1168,36 @@ public final class MentraBluetoothSDK {
1141
1168
  }
1142
1169
  }
1143
1170
 
1171
+ public func queryVideoRecordingStatus(requestId: String) async throws -> VideoRecordingStatusEvent {
1172
+ guard !requestId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
1173
+ throw BluetoothSdkError(
1174
+ code: "missing_request_id",
1175
+ message: "requestId is required to query video recording status."
1176
+ )
1177
+ }
1178
+ try requireGlassesConnected(operation: "query video recording status")
1179
+ let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "query video recording status \(requestId)")
1180
+ guard pendingVideoRecordingRequests[requestId] == nil else {
1181
+ throw BluetoothSdkError(
1182
+ code: "request_in_flight",
1183
+ message: "A video recording command is already waiting for requestId \(requestId)."
1184
+ )
1185
+ }
1186
+ pendingVideoRecordingRequests[requestId] = PendingVideoRecordingRequest(
1187
+ expectedStatus: "recording_status",
1188
+ pending: pending
1189
+ )
1190
+ DeviceManager.shared.queryVideoRecordingStatus(requestId)
1191
+ do {
1192
+ let event = try await pending.wait()
1193
+ pendingVideoRecordingRequests.removeValue(forKey: requestId)
1194
+ return event
1195
+ } catch {
1196
+ pendingVideoRecordingRequests.removeValue(forKey: requestId)
1197
+ throw error
1198
+ }
1199
+ }
1200
+
1144
1201
  public func requestVersionInfo() async throws -> VersionInfoResult {
1145
1202
  guard pendingVersionInfo == nil else {
1146
1203
  throw BluetoothSdkError(
@@ -1165,11 +1222,13 @@ public final class MentraBluetoothSDK {
1165
1222
  }
1166
1223
  }
1167
1224
 
1168
- func setOtaVersionUrl(_ otaVersionUrl: String) throws {
1225
+ /// Select the HTTP(S) manifest used by subsequent OTA checks and installs.
1226
+ public func setOtaVersionUrl(_ otaVersionUrl: String) throws {
1169
1227
  configuredOtaVersionUrl = try OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
1170
1228
  }
1171
1229
 
1172
- func getOtaVersionUrl() throws -> String {
1230
+ /// Return the configured OTA manifest, or this SDK release's default manifest.
1231
+ public func getOtaVersionUrl() throws -> String {
1173
1232
  try configuredOtaVersionUrl ?? OtaManifestDefaults.defaultOtaVersionUrl()
1174
1233
  }
1175
1234
 
@@ -1200,6 +1259,14 @@ public final class MentraBluetoothSDK {
1200
1259
  )
1201
1260
  }
1202
1261
 
1262
+ /// Return bundled release changelogs crossed between two coordinated product versions, newest first.
1263
+ public func getReleaseChangelogs(
1264
+ fromVersion: String? = nil,
1265
+ toVersion: String? = nil
1266
+ ) throws -> [ReleaseChangelog] {
1267
+ try ReleaseChangelogCatalog.select(fromVersion: fromVersion, toVersion: toVersion)
1268
+ }
1269
+
1203
1270
  /// Ask connected Mentra Live glasses to report the current OTA install/session status.
1204
1271
  private func queryOtaStatus() async throws -> OtaQueryResult {
1205
1272
  try await performOtaQuery(operation: "OTA status query") {
@@ -2057,6 +2124,8 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2057
2124
  if !event.lc3.isEmpty {
2058
2125
  delegate?.mentraBluetoothSDK(self, didReceiveMicLc3: event)
2059
2126
  }
2127
+ case "mic_health":
2128
+ delegate?.mentraBluetoothSDK(self, didReceive: .micHealth(MicHealthEvent(values: data)))
2060
2129
  case "local_transcription":
2061
2130
  let event = LocalTranscriptionEvent(
2062
2131
  text: data["text"] as? String ?? "",
@@ -2105,6 +2174,10 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2105
2174
  let event = GalleryStatusEvent(values: data)
2106
2175
  pendingGalleryStatus?.resolve(event)
2107
2176
  delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "gallery_status", values: event.values))
2177
+ case "pairing_info":
2178
+ delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "pairing_info", values: data))
2179
+ case "entering_pairing_mode", "owner_replaced":
2180
+ delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
2108
2181
  case "photo_response":
2109
2182
  let event = PhotoResponseEvent(values: data)
2110
2183
  handlePhotoResponseForRequests(event)
@@ -2147,7 +2220,13 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2147
2220
  var resultValues = data
2148
2221
  resultValues["type"] = "ota_status"
2149
2222
  pendingOtaQuery?.resolve(OtaQueryResult(values: resultValues))
2150
- delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(OtaStatusEvent(values: resultValues)))
2223
+ let event = OtaStatusEvent(values: resultValues)
2224
+ if let errorCode = otaStartRejectionErrorCode(event) {
2225
+ pendingOtaStart?.reject(
2226
+ BluetoothSdkError(code: errorCode, message: "Glasses rejected OTA start: \(errorCode)")
2227
+ )
2228
+ }
2229
+ delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(event))
2151
2230
  case "settings_ack":
2152
2231
  let event = SettingsAckEvent(values: data)
2153
2232
  handleSettingsAckForRequests(event)
@@ -2172,6 +2251,12 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
2172
2251
  }
2173
2252
  }
2174
2253
 
2175
-
2176
-
2177
-
2254
+ /// OTA status messages are not request-correlated, so only known pre-ack failures settle a start.
2255
+ private func otaStartRejectionErrorCode(_ event: OtaStatusEvent) -> String? {
2256
+ guard event.status.lowercased() == "failed",
2257
+ event.errorMessage?.lowercased() == "battery_low"
2258
+ else {
2259
+ return nil
2260
+ }
2261
+ return "battery_low"
2262
+ }
@@ -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
 
@@ -0,0 +1,65 @@
1
+ import Foundation
2
+
3
+ /// One coordinated Mentra release note authored in /changelogs/<version>.md.
4
+ public struct ReleaseChangelog: Equatable, Sendable {
5
+ public let version: String
6
+ public let markdown: String
7
+
8
+ public init(version: String, markdown: String) {
9
+ self.version = version
10
+ self.markdown = markdown
11
+ }
12
+ }
13
+
14
+ enum ReleaseChangelogCatalog {
15
+ static func select(fromVersion: String?, toVersion: String?) throws -> [ReleaseChangelog] {
16
+ let fallbackTarget = GeneratedReleaseMetadata.familyBaseVersion.isEmpty
17
+ ? (generatedReleaseChangelogs.first?.version ?? "")
18
+ : GeneratedReleaseMetadata.familyBaseVersion
19
+ if fallbackTarget.isEmpty, toVersion == nil { return [] }
20
+ let target = try baseVersion(toVersion ?? fallbackTarget, label: "toVersion")
21
+ guard generatedReleaseChangelogs.contains(where: { $0.version == target }) else {
22
+ throw BluetoothSdkError(code: "missing_changelog", message: "No changelog is bundled for target version \(target).")
23
+ }
24
+ guard let fromVersion else {
25
+ return generatedReleaseChangelogs.filter { $0.version == target }
26
+ }
27
+
28
+ let source = try baseVersion(fromVersion, label: "fromVersion")
29
+ let direction = compareVersions(target, source)
30
+ return generatedReleaseChangelogs.filter { entry in
31
+ if direction == 0 { return entry.version == target }
32
+ if direction > 0 {
33
+ return compareVersions(entry.version, source) > 0 && compareVersions(entry.version, target) <= 0
34
+ }
35
+ return compareVersions(entry.version, source) < 0 && compareVersions(entry.version, target) >= 0
36
+ }
37
+ }
38
+
39
+ private static func baseVersion(_ version: String, label: String) throws -> String {
40
+ let value = version.trimmingCharacters(in: .whitespacesAndNewlines)
41
+ let suffixStart = value.firstIndex(where: { $0 == "-" || $0 == "+" }) ?? value.endIndex
42
+ let components = value[..<suffixStart].split(separator: ".", omittingEmptySubsequences: false)
43
+ let isValid = components.count == 3 && components.allSatisfy { component in
44
+ !component.isEmpty
45
+ && (component == "0" || !component.hasPrefix("0"))
46
+ && component.utf8.allSatisfy { byte in byte >= 48 && byte <= 57 }
47
+ }
48
+ guard isValid else {
49
+ throw BluetoothSdkError(
50
+ code: "invalid_changelog_version",
51
+ message: "\(label) must be a semantic version such as 3.1.0, 3.1.0-dev.4, or 3.1.0-beta.2."
52
+ )
53
+ }
54
+ return components.joined(separator: ".")
55
+ }
56
+
57
+ private static func compareVersions(_ left: String, _ right: String) -> Int {
58
+ let a = left.split(separator: ".").map { Int($0) ?? 0 }
59
+ let b = right.split(separator: ".").map { Int($0) ?? 0 }
60
+ for index in 0 ..< 3 where a[index] != b[index] {
61
+ return a[index] - b[index]
62
+ }
63
+ return 0
64
+ }
65
+ }
@@ -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):