@mentra/bluetooth-sdk 0.1.21-beta.0 → 0.1.21-beta.2

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 (89) hide show
  1. package/README.md +22 -11
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/AndroidManifest.xml +3 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +43 -3
  7. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +7 -2
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +82 -6
  9. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -1
  10. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +32 -3
  11. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
  12. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -4
  13. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +161 -14
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +41 -1
  25. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
  29. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +30 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
  36. package/build/BluetoothSdk.types.d.ts +58 -25
  37. package/build/BluetoothSdk.types.d.ts.map +1 -1
  38. package/build/BluetoothSdk.types.js +2 -1
  39. package/build/BluetoothSdk.types.js.map +1 -1
  40. package/build/_internal.d.ts +1 -0
  41. package/build/_internal.d.ts.map +1 -1
  42. package/build/_internal.js +1 -0
  43. package/build/_internal.js.map +1 -1
  44. package/build/_private/BluetoothSdkModule.d.ts +8 -2
  45. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  46. package/build/_private/BluetoothSdkModule.js +6 -3
  47. package/build/_private/BluetoothSdkModule.js.map +1 -1
  48. package/build/_private/cameraRequestPayload.d.ts +1 -1
  49. package/build/_private/cameraRequestPayload.d.ts.map +1 -1
  50. package/build/_private/cameraRequestPayload.js +6 -0
  51. package/build/_private/cameraRequestPayload.js.map +1 -1
  52. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  53. package/build/_private/photoRequestPayload.js +10 -1
  54. package/build/_private/photoRequestPayload.js.map +1 -1
  55. package/build/_private/sdkOtaManifest.d.ts +20 -0
  56. package/build/_private/sdkOtaManifest.d.ts.map +1 -0
  57. package/build/_private/sdkOtaManifest.js +28 -0
  58. package/build/_private/sdkOtaManifest.js.map +1 -0
  59. package/build/index.d.ts +1 -1
  60. package/build/index.d.ts.map +1 -1
  61. package/build/index.js +7 -1
  62. package/build/index.js.map +1 -1
  63. package/ios/BluetoothSdkModule.swift +41 -2
  64. package/ios/Source/BluetoothSdkDefaults.swift +2 -1
  65. package/ios/Source/Bridge.swift +15 -3
  66. package/ios/Source/DeviceManager.swift +67 -17
  67. package/ios/Source/DeviceStore.swift +5 -1
  68. package/ios/Source/MentraBluetoothSDK.swift +37 -8
  69. package/ios/Source/OtaManifest.swift +47 -4
  70. package/ios/Source/camera/CameraModels.swift +20 -5
  71. package/ios/Source/internal/BleTraceLogger.swift +1 -1
  72. package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
  73. package/ios/Source/sgcs/Ar99.swift +2451 -0
  74. package/ios/Source/sgcs/MentraLive.swift +174 -38
  75. package/ios/Source/sgcs/SGCManager.swift +8 -0
  76. package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
  77. package/ios/Source/streaming/StreamModels.swift +71 -7
  78. package/ios/Source/types/DeviceModels.swift +13 -0
  79. package/ios/Source/utils/BleJsonCompact.swift +8 -0
  80. package/ios/Source/utils/Constants.swift +2 -0
  81. package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
  82. package/package.json +1 -1
  83. package/src/BluetoothSdk.types.ts +61 -28
  84. package/src/_internal.ts +1 -0
  85. package/src/_private/BluetoothSdkModule.ts +20 -6
  86. package/src/_private/cameraRequestPayload.ts +8 -2
  87. package/src/_private/photoRequestPayload.ts +12 -2
  88. package/src/_private/sdkOtaManifest.ts +31 -0
  89. package/src/index.ts +9 -1
@@ -217,25 +217,79 @@ public struct StreamResolvedConfig: Equatable {
217
217
  }
218
218
  }
219
219
 
220
+ private func streamInt64Value(_ value: Any?) -> Int64? {
221
+ if let int64 = value as? Int64 { return int64 }
222
+ if let int = value as? Int { return Int64(int) }
223
+ if let number = value as? NSNumber { return number.int64Value }
224
+ return nil
225
+ }
226
+
227
+ /// Live encoder and device telemetry reported by the glasses while streaming.
228
+ public struct StreamLiveStats: Equatable {
229
+ public let bitrate: Int64?
230
+ public let fps: Double?
231
+ public let droppedFrames: Int64?
232
+ public let duration: Int64?
233
+ public let temperatureC: Double?
234
+
235
+ public init(
236
+ bitrate: Int64? = nil,
237
+ fps: Double? = nil,
238
+ droppedFrames: Int64? = nil,
239
+ duration: Int64? = nil,
240
+ temperatureC: Double? = nil
241
+ ) {
242
+ self.bitrate = bitrate
243
+ self.fps = fps
244
+ self.droppedFrames = droppedFrames
245
+ self.duration = duration
246
+ self.temperatureC = temperatureC
247
+ }
248
+
249
+ init?(values: [String: Any]?) {
250
+ guard let values else { return nil }
251
+ self.init(
252
+ bitrate: streamInt64Value(values["bitrate"]),
253
+ fps: doubleValue(values["fps"]),
254
+ droppedFrames: streamInt64Value(values["droppedFrames"]),
255
+ duration: streamInt64Value(values["duration"]),
256
+ temperatureC: doubleValue(values["temperatureC"])
257
+ )
258
+ }
259
+
260
+ var values: [String: Any] {
261
+ var values: [String: Any] = [:]
262
+ if let bitrate { values["bitrate"] = bitrate }
263
+ if let fps { values["fps"] = fps }
264
+ if let droppedFrames { values["droppedFrames"] = droppedFrames }
265
+ if let duration { values["duration"] = duration }
266
+ if let temperatureC { values["temperatureC"] = temperatureC }
267
+ return values
268
+ }
269
+ }
270
+
220
271
  public struct StreamRequest {
221
272
  public let streamUrl: String
222
273
  public let streamId: String
223
274
  public let sound: Bool
224
275
  public let video: StreamVideoConfig?
225
276
  public let audio: StreamAudioConfig?
277
+ public let authToken: String?
226
278
 
227
279
  public init(
228
280
  streamUrl: String,
229
281
  streamId: String = "",
230
282
  sound: Bool = true,
231
283
  video: StreamVideoConfig? = nil,
232
- audio: StreamAudioConfig? = nil
284
+ audio: StreamAudioConfig? = nil,
285
+ authToken: String? = nil
233
286
  ) {
234
287
  self.streamUrl = streamUrl
235
288
  self.streamId = streamId
236
289
  self.sound = sound
237
290
  self.video = video
238
291
  self.audio = audio
292
+ self.authToken = authToken
239
293
  }
240
294
 
241
295
  init(values: [String: Any]) {
@@ -248,7 +302,8 @@ public struct StreamRequest {
248
302
  streamId: values["streamId"] as? String ?? "",
249
303
  sound: values["sound"] as? Bool ?? true,
250
304
  video: StreamVideoConfig(values: values["video"] as? [String: Any]),
251
- 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
252
307
  )
253
308
  }
254
309
 
@@ -264,6 +319,9 @@ public struct StreamRequest {
264
319
  if let audioValues = audio?.dictionary, !audioValues.isEmpty {
265
320
  values["audio"] = audioValues
266
321
  }
322
+ if let authToken, !authToken.isEmpty {
323
+ values["authToken"] = authToken
324
+ }
267
325
  return values
268
326
  }
269
327
  }
@@ -553,18 +611,21 @@ public enum StreamStatus: CustomStringConvertible, Equatable {
553
611
 
554
612
  public struct StreamStatusEvent: CustomStringConvertible {
555
613
  public let status: StreamStatus
556
- // True when the glasses will retry the failed publisher themselves
557
- // (emitting side lands in PR #3488); absent on older firmware and on
558
- // events not parsed from a glasses status map. Carried here instead of
559
- // as an `.error` associated value so the public case stays unchanged.
614
+ public let stats: StreamLiveStats?
615
+ /// True when the glasses will retry the failed publisher themselves
616
+ /// (emitting side lands in PR #3488); absent on older firmware and on
617
+ /// events not parsed from a glasses status map. Carried here instead of
618
+ /// as an `.error` associated value so the public case stays unchanged.
560
619
  public private(set) var willRetry: Bool?
561
620
 
562
- public init(status: StreamStatus) {
621
+ public init(status: StreamStatus, stats: StreamLiveStats? = nil) {
563
622
  self.status = status
623
+ self.stats = stats
564
624
  }
565
625
 
566
626
  public init(values: [String: Any]) {
567
627
  status = StreamStatus(values: values)
628
+ stats = StreamLiveStats(values: values["stats"] as? [String: Any])
568
629
  willRetry = boolValue(values, "willRetry")
569
630
  }
570
631
 
@@ -583,6 +644,9 @@ public struct StreamStatusEvent: CustomStringConvertible {
583
644
  public var values: [String: Any] {
584
645
  var values = status.values
585
646
  values["type"] = "stream_status"
647
+ if let stats {
648
+ values["stats"] = stats.values
649
+ }
586
650
  if let willRetry {
587
651
  values["willRetry"] = willRetry
588
652
  }
@@ -19,6 +19,7 @@ public enum DeviceModel: String {
19
19
  case z100
20
20
  case frame
21
21
  case nimo
22
+ case ar99
22
23
  case simulated
23
24
  case r1
24
25
 
@@ -40,6 +41,8 @@ public enum DeviceModel: String {
40
41
  DeviceTypes.FRAME
41
42
  case .nimo:
42
43
  DeviceTypes.NIMO
44
+ case .ar99:
45
+ DeviceTypes.AR99
43
46
  case .simulated:
44
47
  DeviceTypes.SIMULATED
45
48
  case .r1:
@@ -65,6 +68,8 @@ public enum DeviceModel: String {
65
68
  .frame
66
69
  case DeviceTypes.NIMO:
67
70
  .nimo
71
+ case DeviceTypes.AR99:
72
+ .ar99
68
73
  case DeviceTypes.SIMULATED:
69
74
  .simulated
70
75
  case ControllerTypes.R1:
@@ -80,6 +85,7 @@ public struct Device: Identifiable, Equatable, CustomStringConvertible {
80
85
  public let name: String
81
86
  /// CoreBluetooth identifier when available.
82
87
  public let identifier: String?
88
+ public let projectName: String?
83
89
  public let rssi: Int?
84
90
  /// Stable app-facing scan-result key. Do not parse; use typed fields instead.
85
91
  public let id: String
@@ -88,12 +94,14 @@ public struct Device: Identifiable, Equatable, CustomStringConvertible {
88
94
  model: DeviceModel,
89
95
  name: String,
90
96
  identifier: String? = nil,
97
+ projectName: String? = nil,
91
98
  rssi: Int? = nil,
92
99
  id: String? = nil
93
100
  ) {
94
101
  self.model = model
95
102
  self.name = name
96
103
  self.identifier = identifier
104
+ self.projectName = projectName
97
105
  self.rssi = rssi
98
106
  self.id = id ?? identifier.flatMap { $0.isEmpty ? nil : $0 } ?? "\(model.deviceType):\(name)"
99
107
  }
@@ -111,6 +119,9 @@ public struct Device: Identifiable, Equatable, CustomStringConvertible {
111
119
  if let identifier, !identifier.isEmpty {
112
120
  values["address"] = identifier
113
121
  }
122
+ if let projectName, !projectName.isEmpty {
123
+ values["projectName"] = projectName
124
+ }
114
125
  if let rssi {
115
126
  values["rssi"] = rssi
116
127
  }
@@ -121,11 +132,13 @@ public struct Device: Identifiable, Equatable, CustomStringConvertible {
121
132
  guard let model = stringValue(values, "model") else { return nil }
122
133
  guard let name = stringValue(values, "name") else { return nil }
123
134
  let identifier = stringValue(values, "address").flatMap { $0.isEmpty ? nil : $0 }
135
+ let projectName = stringValue(values, "projectName").flatMap { $0.isEmpty ? nil : $0 }
124
136
  let rssi = intValue(values["rssi"])
125
137
  self.init(
126
138
  model: DeviceModel.fromDeviceType(model),
127
139
  name: name,
128
140
  identifier: identifier,
141
+ projectName: projectName,
129
142
  rssi: rssi,
130
143
  id: stringValue(values, "id")
131
144
  )
@@ -19,6 +19,14 @@ enum BleJsonCompact {
19
19
  "transferMethod": "tm",
20
20
  "manual": "m",
21
21
  "reconnecting": "rc",
22
+ "stats": "st",
23
+ "width": "w",
24
+ "height": "h",
25
+ "bitrate": "br",
26
+ "fps": "f",
27
+ "droppedFrames": "df",
28
+ "duration": "du",
29
+ "temperatureC": "tc",
22
30
  ]
23
31
 
24
32
  private static let shortToLong: [String: String] = {
@@ -8,6 +8,7 @@ struct DeviceTypes {
8
8
  static let NEX = "Mentra Display"
9
9
  static let FRAME = "Brilliant Frame"
10
10
  static let NIMO = "NIMO"
11
+ static let AR99 = "AR99"
11
12
 
12
13
  static let ALL = [
13
14
  SIMULATED,
@@ -19,6 +20,7 @@ struct DeviceTypes {
19
20
  NEX,
20
21
  FRAME,
21
22
  NIMO,
23
+ AR99,
22
24
  ]
23
25
 
24
26
  /// Private init to prevent instantiation
@@ -55,6 +55,10 @@ class PhoneAudioMonitor {
55
55
  return session.isOtherAudioPlaying || ownAppAudioPlaying
56
56
  }
57
57
 
58
+ func isOwnAppAudioPlaying() -> Bool {
59
+ ownAppAudioPlaying
60
+ }
61
+
58
62
  /// Notify the monitor that our own app started/stopped playing audio
59
63
  /// Called from RN AudioPlaybackService via Bridge
60
64
  func setOwnAppAudioPlaying(_ playing: Bool) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.21-beta.0",
3
+ "version": "0.1.21-beta.2",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -5,7 +5,7 @@ export type GlassesNotReadyEvent = {
5
5
  }
6
6
 
7
7
  // NOTE: unlike most events below, the native module does NOT include a `type`
8
- // field on the button_press payload — it sends only {buttonId, pressType,
8
+ // field on the button_press payload 闂?it sends only {buttonId, pressType,
9
9
  // timestamp} (see BluetoothSdkModule on both iOS and Android). Consumers must
10
10
  // filter on `pressType` / the "button_press" listener name, never `event.type`.
11
11
  export type ButtonPressEvent = {
@@ -85,7 +85,7 @@ export function createDisconnectedGlassesStatus(): Partial<GlassesStatus> {
85
85
  }
86
86
  }
87
87
 
88
- /** K900 `sr_getvol` response (Mentra Live glasses media step volume 0–15). */
88
+ /** K900 `sr_getvol` response (Mentra Live glasses media step volume 0闂?5). */
89
89
  export type GlassesMediaVolumeGetResult = {
90
90
  level: number
91
91
  statusCode: number
@@ -119,11 +119,11 @@ export type WifiStatusChangeEvent = WifiStatus & {
119
119
  /**
120
120
  * Glasses-reported provisioning failure reason when THIS event is the verdict of a
121
121
  * failed connect attempt; absent on routine link-state updates. An attempt property,
122
- * not a link property — which is why it lives on the event, not on WifiStatus:
122
+ * not a link property 闂?which is why it lives on the event, not on WifiStatus:
123
123
  * "connect_timeout" arrives on a disconnected status (never associated), while
124
124
  * "connected_to_other_network" arrives on a *connected* status (the attempt failed
125
125
  * and the glasses ended up on / fell back to a different SSID than requested).
126
- * Requires ASG client v40+ — older glasses never send it.
126
+ * Requires ASG client v40+ 闂?older glasses never send it.
127
127
  */
128
128
  error?: string
129
129
  }
@@ -453,17 +453,21 @@ export type RgbLedAction = "on" | "off"
453
453
  export type RgbLedColor = "red" | "green" | "blue" | "orange" | "white"
454
454
  export type PhotoSize = "low" | "medium" | "high" | "max"
455
455
  export type PhotoMode = "photo" | "text"
456
+ export type PhotoTransferMethod = "auto" | "direct" | "ble"
456
457
  export type ButtonPhotoSize = "low" | "medium" | "high" | "max"
457
458
 
458
459
  /**
459
460
  * @deprecated Sticky action-button photo presets via {@link BluetoothSdkPublicModule.setPhotoCaptureDefaults}
460
461
  * are deprecated. Prefer per-request {@link BluetoothSdkPublicModule.requestPhoto} options
461
- * (e.g. `mode: "text"` for AE ÷3) instead of persisting button-photo tuning on the glasses.
462
+ * (e.g. `mode: "text"` for text sensor size/crop, or explicit `aeExposureDivisor`) instead of
463
+ * persisting button-photo tuning on the glasses.
462
464
  */
463
465
  export type PhotoCaptureDefaults = {
464
466
  size?: PhotoSize
465
- mfnr?: boolean
467
+ /** ZSL preview buffering for physical camera-button photos. */
466
468
  zsl?: boolean
469
+ /** MFNR still capture for physical camera-button photos. */
470
+ mfnr?: boolean
467
471
  noiseReduction?: boolean
468
472
  edgeEnhancement?: boolean
469
473
  ispDigitalGain?: number
@@ -509,6 +513,7 @@ export const DeviceModels = {
509
513
  Z100: "Vuzix Z100",
510
514
  Frame: "Brilliant Frame",
511
515
  Nimo: "NIMO",
516
+ Ar99: "AR99",
512
517
  R1: "Even Realities R1",
513
518
  } as const
514
519
 
@@ -523,7 +528,7 @@ export type DashboardMenuItem = {
523
528
 
524
529
  export const CAMERA_FOV_MIN = 62
525
530
  export const CAMERA_FOV_MAX = 118
526
- export const CAMERA_FOV_DEFAULT = 102
531
+ export const CAMERA_FOV_DEFAULT = CAMERA_FOV_MAX
527
532
 
528
533
  export type CameraRoiPosition = "center" | "bottom" | "top"
529
534
  export type CameraRoiPositionValue = 0 | 1 | 2
@@ -570,8 +575,8 @@ export type PhotoRequestParams = {
570
575
  appId?: string
571
576
  size: PhotoSize
572
577
  mode?: PhotoMode
573
- /** `ble` skips direct upload and forces phone-relayed BLE image transfer. */
574
- transferMethod?: "auto" | "ble"
578
+ /** `direct` disables BLE fallback; `ble` skips direct upload and forces phone-relayed transfer. */
579
+ transferMethod?: PhotoTransferMethod
575
580
  webhookUrl: string | null
576
581
  authToken: string | null
577
582
  compress: PhotoCompression
@@ -587,8 +592,10 @@ export type PhotoRequestParams = {
587
592
  /** Requested on wire; glasses may log not_implemented. */
588
593
  noiseReduction?: boolean
589
594
  edgeEnhancement?: boolean
590
- mfnr?: boolean
595
+ /** ZSL buffering. Forced off for manual/scan stills because fixed sensor controls take priority. */
591
596
  zsl?: boolean
597
+ /** MFNR still capture. Forced off for manual/scan stills because fixed sensor controls take priority. */
598
+ mfnr?: boolean
592
599
  ispDigitalGain?: number
593
600
  ispAnalogGain?: string
594
601
  }
@@ -601,6 +608,10 @@ export type WarmUpCameraParams = {
601
608
  exposureTimeNs?: number | null
602
609
  /** Ready-state hold; defaults to 15 seconds and is capped at 60 seconds by ASG. */
603
610
  durationMs?: number
611
+ /** ZSL preview buffering for the warm-up session. */
612
+ zsl?: boolean
613
+ /** MFNR still capture for the warm-up session. */
614
+ mfnr?: boolean
604
615
  }
605
616
 
606
617
  export type StreamVideoConfig = {
@@ -721,7 +732,7 @@ export type StreamResolvedConfig = {
721
732
  }
722
733
  }
723
734
 
724
- /** Live encoder telemetry, when the glasses emit it (no firmware does yet). */
735
+ /** Live encoder and device telemetry emitted periodically by supported glasses firmware. */
725
736
  export type StreamLiveStats = {
726
737
  /** Current encoded video bitrate in bits per second. */
727
738
  bitrate?: number
@@ -730,7 +741,7 @@ export type StreamLiveStats = {
730
741
  droppedFrames?: number
731
742
  /** Seconds since the stream started. */
732
743
  duration?: number
733
- /** Device temperature in °C, if the hardware reports it. */
744
+ /** Device temperature in 闂佺娅i悡? if the hardware reports it. */
734
745
  temperatureC?: number
735
746
  }
736
747
 
@@ -790,6 +801,17 @@ export type MtkUpdateCompleteEvent = {
790
801
  timestamp: number
791
802
  }
792
803
 
804
+ /**
805
+ * The glasses process restarted while the BES kept the BLE link alive (its `sid`
806
+ * changed, or first appeared after an update from a pre-sid build). There is no
807
+ * physical disconnect for this — treat it as the logical reconnect edge.
808
+ */
809
+ export type GlassesSessionChangedEvent = {
810
+ type: "glasses_session_changed"
811
+ previous_sid: string
812
+ sid: string
813
+ }
814
+
793
815
  /** @deprecated Glasses no longer emit ota_progress; use {@link OtaStatusEvent} and status-store mapping. */
794
816
  export type OtaProgressEvent = {
795
817
  type: "ota_progress"
@@ -806,16 +828,6 @@ export type OtaStartAckEvent = {
806
828
  type: "ota_start_ack"
807
829
  timestamp: number
808
830
  }
809
-
810
- export type OtaUpdateAvailableEvent = {
811
- type: "ota_update_available"
812
- version_code?: number
813
- version_name?: string
814
- updates?: string[]
815
- total_size?: number
816
- cache_ready?: boolean
817
- }
818
-
819
831
  export type OtaStatusEvent = {
820
832
  type: "ota_status"
821
833
  session_id: string
@@ -894,9 +906,10 @@ export type BluetoothSdkModuleEvents = {
894
906
  stream_status: (event: StreamStatusEvent) => void
895
907
  keep_alive_ack: (event: KeepAliveAckEvent) => void
896
908
  mtk_update_complete: (event: MtkUpdateCompleteEvent) => void
897
- ota_update_available: (event: OtaUpdateAvailableEvent) => void
909
+ glasses_session_changed: (event: GlassesSessionChangedEvent) => void
898
910
  ota_start_ack: (event: OtaStartAckEvent) => void
899
911
  ota_status: (event: OtaStatusEvent) => void
912
+ ar99_ota_status: (event: Ar99OtaStatusEvent) => void
900
913
  version_info: (event: VersionInfoEvent) => void
901
914
  send_command_to_ble: (event: BleCommandTraceEvent) => void
902
915
  receive_command_from_ble: (event: BleCommandTraceEvent) => void
@@ -910,6 +923,16 @@ export interface ExtractionProgressEvent {
910
923
  totalBytes: number
911
924
  }
912
925
 
926
+ export interface Ar99OtaStatusEvent {
927
+ type: "ar99_ota_status"
928
+ phase: string
929
+ progress: number
930
+ offset: number
931
+ total: number
932
+ errorMessage?: string
933
+ error_message?: string
934
+ }
935
+
913
936
  export interface PhoneNotificationEvent {
914
937
  notificationId: string
915
938
  app: string
@@ -982,9 +1005,9 @@ export type BluetoothSdkEventMap = {
982
1005
  mic_pcm: MicPcmEvent
983
1006
  mic_lc3: MicLc3Event
984
1007
  stream_status: StreamStatusEvent
985
- ota_update_available: OtaUpdateAvailableEvent
986
1008
  ota_start_ack: OtaStartAckEvent
987
1009
  ota_status: OtaStatusEvent
1010
+ ar99_ota_status: Ar99OtaStatusEvent
988
1011
  version_info: VersionInfoEvent
989
1012
  extraction_progress: ExtractionProgressEvent
990
1013
  }
@@ -1036,9 +1059,10 @@ export interface BluetoothSdkPublicModule {
1036
1059
 
1037
1060
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
1038
1061
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
1062
+ setLoudnessGateEnabled(enabled: boolean): Promise<void>
1039
1063
  /**
1040
1064
  * @deprecated Sticky action-button photo presets are deprecated. Prefer per-request
1041
- * `requestPhoto(...)` options (e.g. `mode: "text"` for AE ÷3, or explicit per-shot
1065
+ * `requestPhoto(...)` options (e.g. `mode: "text"` for text sensor size/crop, or explicit per-shot
1042
1066
  * fields). Still functional until removed in a future release.
1043
1067
  */
1044
1068
  setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): Promise<SettingsAckSuccessEvent>
@@ -1078,7 +1102,7 @@ export interface BluetoothSdkPublicModule {
1078
1102
  * Stop the active recording. When {@link webhookUrl} is provided, the glasses
1079
1103
  * upload the recorded video to it (multipart) using {@link authToken}. These
1080
1104
  * are supplied at stop time (not start) so the token is fresh when the upload
1081
- * runs — a recording can last arbitrarily long. An empty/omitted webhook keeps
1105
+ * runs 闂?a recording can last arbitrarily long. An empty/omitted webhook keeps
1082
1106
  * the video on device (no upload).
1083
1107
  */
1084
1108
  stopVideoRecording(
@@ -1111,6 +1135,10 @@ export interface BluetoothSdkPublicModule {
1111
1135
  checkForOtaUpdate(): Promise<boolean>
1112
1136
  /** Start the OTA flow with the same configured manifest URL used by checkForOtaUpdate(). */
1113
1137
  startOtaUpdate(): Promise<OtaStartAckEvent>
1138
+ startAr99OtaFromFile(path: string): Promise<boolean>
1139
+ cancelAr99Ota(): Promise<void>
1140
+ sendAr99FactoryReset(): Promise<void>
1141
+ buildAr99OtaSignature(secret: string, appName: string, currentVersion: string, serialNumber: string, nonce: string): string
1114
1142
 
1115
1143
  // // stt commands (MOVE TO CRUST)
1116
1144
  // setSttModelDetails(path: string, languageCode: string): Promise<void>
@@ -1173,6 +1201,8 @@ export interface OtaUpdateInfo {
1173
1201
  updates: string[] // ["apk", "mtk", "bes"]
1174
1202
  totalSize: number
1175
1203
  cacheReady?: boolean
1204
+ /** True when the APK step installs an older build than the glasses currently run (exact-pin manifests only). */
1205
+ isDowngrade?: boolean
1176
1206
  }
1177
1207
 
1178
1208
  export interface OtaProgress {
@@ -1257,13 +1287,15 @@ export interface Device {
1257
1287
  /**
1258
1288
  * Stable app-facing key for this scan result, within the limits of the
1259
1289
  * platform identifier available to the SDK. Do not parse this value; use the
1260
- * typed model, name, address, and rssi fields instead.
1290
+ * typed model, name, address, projectName, and rssi fields instead.
1261
1291
  */
1262
1292
  id: string
1263
1293
  model: DeviceModel
1264
1294
  name: string
1265
1295
  /** Platform address/identifier when available: Android Bluetooth address, iOS CoreBluetooth identifier. */
1266
1296
  address?: string
1297
+ /** Optional AR99 project discriminator. Supported value: AR99. */
1298
+ projectName?: string
1267
1299
  /**
1268
1300
  * Optional scan signal strength. It may be undefined at first discovery and
1269
1301
  * appear in a later scan update when the platform reports RSSI metadata.
@@ -1294,7 +1326,7 @@ export interface WifiSearchResult {
1294
1326
  ssid: string
1295
1327
  requiresPassword: boolean
1296
1328
  signalStrength: number
1297
- /** Frequency in MHz (from glasses scan). 5 GHz band is typically 5170–5825. Omitted if unknown. */
1329
+ /** Frequency in MHz (from glasses scan). 5 GHz band is typically 5170闂?825. Omitted if unknown. */
1298
1330
  frequency?: number
1299
1331
  }
1300
1332
 
@@ -1347,6 +1379,7 @@ export type BluetoothSettingsUpdate = Partial<{
1347
1379
  twelve_hour_time: boolean
1348
1380
  gallery_mode: boolean
1349
1381
  voice_activity_detection_enabled: boolean
1382
+ loudness_gate_enabled: boolean
1350
1383
  button_photo_size: ButtonPhotoSize
1351
1384
  button_video_settings: {width: number; height: number; fps: number}
1352
1385
  button_video_width: number
package/src/_internal.ts CHANGED
@@ -11,3 +11,4 @@ export type {BluetoothSdkInternalModule} from "./_private/BluetoothSdkModule"
11
11
  export * from "./BluetoothSdk.types"
12
12
  export {default as MentraLocalNetwork} from "./_private/MentraLocalNetworkModule"
13
13
  export * from "./_private/MentraLocalNetworkModule"
14
+ export {BLUETOOTH_SDK_VERSION, sdkPinnedOtaManifestUrl} from "./_private/sdkOtaManifest"
@@ -125,9 +125,11 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
125
125
  // Gallery Commands
126
126
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
127
127
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
128
+ /** Mentra Live center-mic loudness / Barrier gate (cs_swit type 10). */
129
+ setLoudnessGateEnabled(enabled: boolean): Promise<void>
128
130
  /**
129
131
  * @deprecated Sticky action-button photo presets are deprecated. Prefer per-request
130
- * `requestPhoto(...)` options (e.g. `mode: "text"` for AE ÷3).
132
+ * `requestPhoto(...)` options (e.g. `mode: "text"` for text sensor size/crop).
131
133
  */
132
134
  setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): Promise<SettingsAckSuccessEvent>
133
135
  setVideoRecordingDefaults(width: number, height: number, fps: number): Promise<SettingsAckSuccessEvent>
@@ -157,6 +159,10 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
157
159
  checkForOtaUpdate(): Promise<boolean>
158
160
  startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>
159
161
  sendOtaQueryStatus(): Promise<OtaQueryResult>
162
+ startAr99OtaFromFile(path: string): Promise<boolean>
163
+ cancelAr99Ota(): Promise<void>
164
+ sendAr99FactoryReset(): Promise<void>
165
+ buildAr99OtaSignature(secret: string, appName: string, currentVersion: string, serialNumber: string, nonce: string): string
160
166
 
161
167
  // Version Info Commands
162
168
  requestVersionInfo(): Promise<VersionInfoResult>
@@ -191,7 +197,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
191
197
  setOwnAppAudioPlaying(playing: boolean): Promise<void>
192
198
 
193
199
  // Live PCM output stream (miniapp speaker.createStream). MentraOS-internal
194
- // — 16-bit LE PCM chunks into a streaming AudioTrack (USAGE_MEDIA, so it
200
+ // ? 16-bit LE PCM chunks into a streaming AudioTrack (USAGE_MEDIA, so it
195
201
  // follows the phone's media route, e.g. A2DP to glasses). Implemented with
196
202
  // AudioTrack on Android and AVAudioEngine on iOS.
197
203
  /** Open a PCM stream session. One AudioTrack per id; caller manages ids. */
@@ -209,7 +215,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
209
215
 
210
216
  /** Mentra Live only: K900 `cs_getvol` / `sr_getvol`. */
211
217
  getGlassesMediaVolume(): Promise<GlassesMediaVolumeGetResult>
212
- /** Mentra Live only: K900 `cs_vol` / `sr_vol`; level clamped 0–15 on native. */
218
+ /** Mentra Live only: K900 `cs_vol` / `sr_vol`; level clamped 0??5 on native. */
213
219
  setGlassesMediaVolume(level: number): Promise<GlassesMediaVolumeSetResult>
214
220
 
215
221
  // RGB LED Control
@@ -270,7 +276,7 @@ const DEFAULT_SCAN_TIMEOUT_MS = 15_000
270
276
  function bindNativeMethod<T extends (...args: never[]) => unknown>(module: Record<string, unknown>, name: string): T {
271
277
  const method = module[name]
272
278
  if (typeof method !== "function") {
273
- console.warn(`[BluetoothSdk] Native method "${name}" is unavailable — rebuild the app (bun android / bun ios)`)
279
+ console.warn(`[BluetoothSdk] Native method "${name}" is unavailable ??rebuild the app (bun android / bun ios)`)
274
280
  return (async () => {
275
281
  throw new Error(`BluetoothSdk.${name} is not available in this native build. Rebuild the app.`)
276
282
  }) as T
@@ -287,10 +293,10 @@ const CAMERA_ROI_POSITION_VALUES: Record<CameraRoiPosition, CameraFovSetting["ro
287
293
  }
288
294
 
289
295
  // Named presets are a convenience layer over the numeric {fov, roiPosition} API.
290
- // "narrow" uses 82, a device-tested FOV; "standard" matches CAMERA_FOV_DEFAULT.
296
+ // The default is the full sensor; "standard" preserves the historical 102� crop.
291
297
  const CAMERA_FOV_PRESETS: Record<CameraFovPreset, CameraFovSetting> = {
292
298
  narrow: {fov: 82, roiPosition: 0},
293
- standard: {fov: CAMERA_FOV_DEFAULT, roiPosition: 0},
299
+ standard: {fov: 102, roiPosition: 0},
294
300
  wide: {fov: CAMERA_FOV_MAX, roiPosition: 0},
295
301
  }
296
302
 
@@ -486,6 +492,10 @@ NativeBluetoothSdkModule.setVoiceActivityDetectionEnabled = function (enabled: b
486
492
  return this.updateBluetoothSettings({voice_activity_detection_enabled: enabled})
487
493
  }
488
494
 
495
+ NativeBluetoothSdkModule.setLoudnessGateEnabled = function (enabled: boolean) {
496
+ return this.updateBluetoothSettings({loudness_gate_enabled: enabled})
497
+ }
498
+
489
499
  const nativeSetCameraFov = bindNativeMethod<(fov: CameraFovSetting) => MaybePromise<CameraFovResult>>(
490
500
  NativeBluetoothSdkModule as unknown as Record<string, unknown>,
491
501
  "setCameraFov",
@@ -639,3 +649,7 @@ NativeBluetoothSdkModule.warmUpCamera = function (params: WarmUpCameraParams) {
639
649
 
640
650
  export default NativeBluetoothSdkModule
641
651
  export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkInternalModule
652
+
653
+
654
+
655
+
@@ -9,8 +9,8 @@ function nonBlankString(value?: string | null): string | undefined {
9
9
  /** Expo Android bridge rejects null values in Map<String, Any> — omit optional nullish fields. */
10
10
  export function warmUpCameraParamsForNative(
11
11
  params: WarmUpCameraParams,
12
- ): Record<string, string | number> {
13
- const payload: Record<string, string | number> = {
12
+ ): Record<string, string | number | boolean> {
13
+ const payload: Record<string, string | number | boolean> = {
14
14
  size: normalizePhotoSizeTier(params.size),
15
15
  mode: params.mode ?? "photo",
16
16
  }
@@ -26,5 +26,11 @@ export function warmUpCameraParamsForNative(
26
26
  if (durationMs != null && Number.isFinite(durationMs) && durationMs > 0) {
27
27
  payload.durationMs = Math.round(durationMs)
28
28
  }
29
+ if (params.mfnr != null) {
30
+ payload.mfnr = params.mfnr
31
+ }
32
+ if (params.zsl != null) {
33
+ payload.zsl = params.zsl
34
+ }
29
35
  return payload
30
36
  }
@@ -1,4 +1,14 @@
1
- import type {PhotoRequestParams, PhotoSize} from "../BluetoothSdk.types"
1
+ import type {PhotoRequestParams, PhotoSize, PhotoTransferMethod} from "../BluetoothSdk.types"
2
+
3
+ const PHOTO_TRANSFER_METHODS = new Set<PhotoTransferMethod>(["auto", "direct", "ble"])
4
+
5
+ function photoTransferMethodForNative(value: unknown): PhotoTransferMethod {
6
+ if (value === undefined) return "auto"
7
+ if (typeof value === "string" && PHOTO_TRANSFER_METHODS.has(value as PhotoTransferMethod)) {
8
+ return value as PhotoTransferMethod
9
+ }
10
+ throw new TypeError(`Invalid transferMethod ${JSON.stringify(value)}. Expected "auto", "direct", or "ble".`)
11
+ }
2
12
 
3
13
  /** Maps unknown/legacy size strings to the current wire format. */
4
14
  export function normalizePhotoSizeTier(size: string | undefined): PhotoSize {
@@ -29,7 +39,7 @@ export function photoRequestParamsForNative(params: PhotoRequestParams): Record<
29
39
  const payload: Record<string, string | number | boolean> = {
30
40
  size: normalizePhotoSizeTier(params.size),
31
41
  mode: params.mode ?? "photo",
32
- transferMethod: params.transferMethod ?? "auto",
42
+ transferMethod: photoTransferMethodForNative(params.transferMethod),
33
43
  webhookUrl: params.webhookUrl ?? "",
34
44
  compress: params.compress,
35
45
  sound: params.sound,