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

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 (83) hide show
  1. package/README.md +6 -2
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +239 -52
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +47 -4
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +6 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +381 -97
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +16 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +293 -174
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +4 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
  19. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +15 -2
  20. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
  21. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
  22. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -8
  23. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +6 -5
  24. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  25. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +31 -0
  27. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
  29. package/build/BluetoothSdk.types.d.ts +71 -1
  30. package/build/BluetoothSdk.types.d.ts.map +1 -1
  31. package/build/BluetoothSdk.types.js.map +1 -1
  32. package/build/_internal.d.ts +2 -0
  33. package/build/_internal.d.ts.map +1 -1
  34. package/build/_internal.js +2 -0
  35. package/build/_internal.js.map +1 -1
  36. package/build/_private/BluetoothSdkModule.d.ts +27 -2
  37. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  38. package/build/_private/BluetoothSdkModule.js +20 -0
  39. package/build/_private/BluetoothSdkModule.js.map +1 -1
  40. package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
  41. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
  42. package/build/_private/MentraLocalNetworkModule.js +3 -0
  43. package/build/_private/MentraLocalNetworkModule.js.map +1 -0
  44. package/build/_private/cameraRequestPayload.d.ts.map +1 -1
  45. package/build/_private/cameraRequestPayload.js +1 -0
  46. package/build/_private/cameraRequestPayload.js.map +1 -1
  47. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  48. package/build/_private/photoRequestPayload.js +2 -0
  49. package/build/_private/photoRequestPayload.js.map +1 -1
  50. package/build/index.d.ts +1 -1
  51. package/build/index.d.ts.map +1 -1
  52. package/build/index.js +9 -0
  53. package/build/index.js.map +1 -1
  54. package/expo-module.config.json +1 -0
  55. package/ios/BluetoothSdkModule.swift +72 -0
  56. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  57. package/ios/Source/Bridge.swift +72 -18
  58. package/ios/Source/DeviceManager.swift +52 -3
  59. package/ios/Source/DeviceStore.swift +6 -1
  60. package/ios/Source/MentraBluetoothSDK.swift +298 -21
  61. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  62. package/ios/Source/camera/CameraModels.swift +22 -3
  63. package/ios/Source/sgcs/G1.swift +2 -2
  64. package/ios/Source/sgcs/G2.swift +19 -7
  65. package/ios/Source/sgcs/Mach1.swift +1 -1
  66. package/ios/Source/sgcs/MentraLive.swift +456 -52
  67. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  68. package/ios/Source/sgcs/MentraNex.swift +1 -1
  69. package/ios/Source/sgcs/Nimo.swift +1 -1
  70. package/ios/Source/sgcs/SGCManager.swift +5 -2
  71. package/ios/Source/sgcs/Simulated.swift +1 -1
  72. package/ios/Source/status/WifiHotspotStatus.swift +21 -2
  73. package/ios/Source/streaming/StreamModels.swift +9 -0
  74. package/ios/Source/tts/TTSTools.swift +1 -1
  75. package/ios/Source/utils/G1Text.swift +30 -1
  76. package/package.json +1 -2
  77. package/src/BluetoothSdk.types.ts +75 -0
  78. package/src/_internal.ts +2 -0
  79. package/src/_private/BluetoothSdkModule.ts +68 -13
  80. package/src/_private/MentraLocalNetworkModule.ts +50 -0
  81. package/src/_private/cameraRequestPayload.ts +1 -0
  82. package/src/_private/photoRequestPayload.ts +3 -3
  83. package/src/index.ts +10 -0
@@ -0,0 +1,183 @@
1
+ import CoreBluetooth
2
+ import Foundation
3
+
4
+ /// Read-only LE L2CAP CoC fast path for Mentra Live file transfers.
5
+ ///
6
+ /// BES sends the same K900 file frames over this channel that it otherwise
7
+ /// sends through the FILE_READ GATT characteristic. The stream is drained on
8
+ /// a dedicated thread so CoreBluetooth credits are returned independently of
9
+ /// React Native and main-thread work.
10
+ final class MentraLiveL2capChannel: NSObject, StreamDelegate {
11
+ private static let readChunkSize = 4096
12
+ private static let maxBufferSize = 64 * 1024
13
+ private static let frameHeaderSize = 5
14
+ private static let frameOverhead = 32
15
+ private static let maxFilePayloadSize = K900ProtocolUtils.FILE_PACK_SIZE
16
+
17
+ private let channel: CBL2CAPChannel
18
+ private let onFileFrame: (Data) -> Void
19
+ private let onClose: () -> Void
20
+ private let stateLock = NSLock()
21
+
22
+ private var receiveBuffer = [UInt8]()
23
+ private var worker: Thread?
24
+ private var closed = false
25
+ private var closeNotified = false
26
+
27
+ init(
28
+ channel: CBL2CAPChannel,
29
+ onFileFrame: @escaping (Data) -> Void,
30
+ onClose: @escaping () -> Void
31
+ ) {
32
+ self.channel = channel
33
+ self.onFileFrame = onFileFrame
34
+ self.onClose = onClose
35
+ super.init()
36
+ }
37
+
38
+ func start() {
39
+ guard worker == nil else { return }
40
+ let thread = Thread { [weak self] in
41
+ self?.runReadLoop()
42
+ }
43
+ thread.name = "MentraLive-L2CAP"
44
+ thread.qualityOfService = .userInitiated
45
+ worker = thread
46
+ thread.start()
47
+ }
48
+
49
+ func close() {
50
+ stateLock.lock()
51
+ let wasClosed = closed
52
+ closed = true
53
+ stateLock.unlock()
54
+
55
+ guard !wasClosed else { return }
56
+ channel.inputStream?.close()
57
+ channel.outputStream?.close()
58
+ }
59
+
60
+ private var isClosed: Bool {
61
+ stateLock.lock()
62
+ defer { stateLock.unlock() }
63
+ return closed
64
+ }
65
+
66
+ private func runReadLoop() {
67
+ guard let input = channel.inputStream, let output = channel.outputStream else {
68
+ notifyClosed()
69
+ return
70
+ }
71
+
72
+ input.delegate = self
73
+ input.schedule(in: .current, forMode: .default)
74
+ output.schedule(in: .current, forMode: .default)
75
+ input.open()
76
+ output.open()
77
+
78
+ while !isClosed {
79
+ _ = RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 0.1))
80
+ }
81
+
82
+ input.close()
83
+ output.close()
84
+ input.remove(from: .current, forMode: .default)
85
+ output.remove(from: .current, forMode: .default)
86
+ input.delegate = nil
87
+ notifyClosed()
88
+ }
89
+
90
+ func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
91
+ guard aStream === channel.inputStream else { return }
92
+
93
+ switch eventCode {
94
+ case .hasBytesAvailable:
95
+ drainInput()
96
+ case .endEncountered, .errorOccurred:
97
+ close()
98
+ default:
99
+ break
100
+ }
101
+ }
102
+
103
+ private func drainInput() {
104
+ guard let input = channel.inputStream else { return }
105
+ var chunk = [UInt8](repeating: 0, count: Self.readChunkSize)
106
+
107
+ while input.hasBytesAvailable, !isClosed {
108
+ let count = input.read(&chunk, maxLength: chunk.count)
109
+ if count > 0 {
110
+ appendAndDispatch(Array(chunk.prefix(count)))
111
+ } else if count < 0 {
112
+ close()
113
+ return
114
+ } else {
115
+ break
116
+ }
117
+ }
118
+ }
119
+
120
+ private func appendAndDispatch(_ bytes: [UInt8]) {
121
+ if receiveBuffer.count + bytes.count > Self.maxBufferSize {
122
+ receiveBuffer.removeAll(keepingCapacity: true)
123
+ }
124
+ guard bytes.count <= Self.maxBufferSize else { return }
125
+ receiveBuffer.append(contentsOf: bytes)
126
+
127
+ while true {
128
+ guard let start = startMarkerIndex() else {
129
+ if receiveBuffer.last == 0x23 {
130
+ receiveBuffer = [0x23]
131
+ } else {
132
+ receiveBuffer.removeAll(keepingCapacity: true)
133
+ }
134
+ return
135
+ }
136
+
137
+ if start > 0 {
138
+ receiveBuffer.removeFirst(start)
139
+ }
140
+ guard receiveBuffer.count >= Self.frameHeaderSize else { return }
141
+
142
+ let payloadSize = (Int(receiveBuffer[3]) << 8) | Int(receiveBuffer[4])
143
+ guard payloadSize <= Self.maxFilePayloadSize else {
144
+ receiveBuffer.removeFirst()
145
+ continue
146
+ }
147
+
148
+ let frameSize = Self.frameOverhead + payloadSize
149
+ guard receiveBuffer.count >= frameSize else { return }
150
+
151
+ guard receiveBuffer[frameSize - 2] == 0x24,
152
+ receiveBuffer[frameSize - 1] == 0x24
153
+ else {
154
+ receiveBuffer.removeFirst()
155
+ continue
156
+ }
157
+
158
+ onFileFrame(Data(receiveBuffer.prefix(frameSize)))
159
+ receiveBuffer.removeFirst(frameSize)
160
+ }
161
+ }
162
+
163
+ private func startMarkerIndex() -> Int? {
164
+ guard receiveBuffer.count >= 2 else { return nil }
165
+ for index in 0 ..< (receiveBuffer.count - 1)
166
+ where receiveBuffer[index] == 0x23 && receiveBuffer[index + 1] == 0x23
167
+ {
168
+ return index
169
+ }
170
+ return nil
171
+ }
172
+
173
+ private func notifyClosed() {
174
+ stateLock.lock()
175
+ guard !closeNotified else {
176
+ stateLock.unlock()
177
+ return
178
+ }
179
+ closeNotified = true
180
+ stateLock.unlock()
181
+ onClose()
182
+ }
183
+ }
@@ -376,7 +376,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
376
376
  func dbg1() {}
377
377
  func dbg2() {}
378
378
 
379
- func requestWifiScan() {}
379
+ func requestWifiScan(scanId _: String?) {}
380
380
 
381
381
  func sendWifiCredentials(_: String, _: String) {}
382
382
 
@@ -1066,7 +1066,7 @@ class Nimo: NSObject, SGCManager {
1066
1066
 
1067
1067
  // MARK: - SGCManager: Network (no WiFi)
1068
1068
 
1069
- func requestWifiScan() {}
1069
+ func requestWifiScan(scanId _: String?) {}
1070
1070
  func sendWifiCredentials(_: String, _: String) {}
1071
1071
  func forgetWifiNetwork(_: String) {}
1072
1072
  func sendHotspotState(_: Bool) {}
@@ -54,6 +54,7 @@ protocol SGCManager {
54
54
  // MARK: - Audio Control
55
55
 
56
56
  func setMicEnabled(_ enabled: Bool)
57
+ var isMicSuspendedForAudio: Bool { get }
57
58
  func sortMicRanking(list: [String]) -> [String]
58
59
 
59
60
  // MARK: - Messaging
@@ -184,7 +185,7 @@ protocol SGCManager {
184
185
 
185
186
  // MARK: - Network Management
186
187
 
187
- func requestWifiScan()
188
+ func requestWifiScan(scanId: String?)
188
189
  func sendWifiCredentials(_ ssid: String, _ password: String)
189
190
  func forgetWifiNetwork(_ ssid: String)
190
191
  func sendHotspotState(_ enabled: Bool)
@@ -217,6 +218,8 @@ protocol SGCManager {
217
218
  /// doesn't seem to work for concurrency reasons :(
218
219
  /// we can make read-only getters for convienence though:
219
220
  extension SGCManager {
221
+ var isMicSuspendedForAudio: Bool { false }
222
+
220
223
  /// Default: no-op. Only G2 renders positioned text containers; other glasses ignore it.
221
224
  func sendPositionedText(
222
225
  _: String, x _: Int32, y _: Int32, width _: Int32, height _: Int32,
@@ -362,7 +365,7 @@ extension SGCManager {
362
365
 
363
366
  func sendVoiceActivityDetectionSetting() {}
364
367
 
365
- /// Default no-op; Mentra Live overrides when phone detects clock skew during gallery sync.
368
+ /// Default no-op; Mentra Live and G2 override to handle phone-detected clock skew.
366
369
  func sendSetSystemTime(_: Int64) {
367
370
  Bridge.log("SGC: sendSetSystemTime not supported")
368
371
  }
@@ -226,7 +226,7 @@ class Simulated: SGCManager {
226
226
 
227
227
  // MARK: - Network Management
228
228
 
229
- func requestWifiScan() {
229
+ func requestWifiScan(scanId _: String?) {
230
230
  Bridge.log("requestWifiScan")
231
231
  }
232
232
 
@@ -133,20 +133,39 @@ public enum WifiStatus: CustomStringConvertible, Equatable {
133
133
  public struct WifiStatusEvent: CustomStringConvertible {
134
134
  public let status: WifiStatus
135
135
 
136
- public init(status: WifiStatus) {
136
+ /// Glasses-reported provisioning failure reason when THIS event is the verdict of a
137
+ /// failed connect attempt; nil for routine link-state updates. An attempt property,
138
+ /// not a link property — which is why it lives on the event and not on `WifiStatus`:
139
+ /// "connect_timeout" arrives with a disconnected status (never associated), while
140
+ /// "connected_to_other_network" arrives with a *connected* status — the attempt
141
+ /// failed and Android's auto-join left the glasses on (or returned them to) a
142
+ /// different SSID than requested, so the link is genuinely up while the request
143
+ /// genuinely failed. Sent by ASG client builds that include the WiFi error
144
+ /// surfacing (v40+); older glasses never set it.
145
+ public let error: String?
146
+
147
+ public init(status: WifiStatus, error: String? = nil) {
137
148
  self.status = status
149
+ self.error = error
138
150
  }
139
151
 
140
152
  init(connected: Bool, ssid: String?, localIp: String?) {
141
153
  status = WifiStatus.fromStoreFields(connected: connected, ssid: ssid, localIp: localIp) ?? .disconnected
154
+ error = nil
142
155
  }
143
156
 
144
157
  init(values: [String: Any]) {
145
158
  status = WifiStatus(values: values) ?? .disconnected
159
+ let rawError = stringValue(values, "error")
160
+ error = rawError?.isEmpty == false ? rawError : nil
146
161
  }
147
162
 
148
163
  public var values: [String: Any] {
149
- status.values.merging(["type": "wifi_status_change"]) { _, new in new }
164
+ var body = status.values.merging(["type": "wifi_status_change"]) { _, new in new }
165
+ if let error {
166
+ body["error"] = error
167
+ }
168
+ return body
150
169
  }
151
170
 
152
171
  public var description: String {
@@ -553,6 +553,11 @@ public enum StreamStatus: CustomStringConvertible, Equatable {
553
553
 
554
554
  public struct StreamStatusEvent: CustomStringConvertible {
555
555
  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.
560
+ public private(set) var willRetry: Bool?
556
561
 
557
562
  public init(status: StreamStatus) {
558
563
  self.status = status
@@ -560,6 +565,7 @@ public struct StreamStatusEvent: CustomStringConvertible {
560
565
 
561
566
  public init(values: [String: Any]) {
562
567
  status = StreamStatus(values: values)
568
+ willRetry = boolValue(values, "willRetry")
563
569
  }
564
570
 
565
571
  public var state: StreamState {
@@ -577,6 +583,9 @@ public struct StreamStatusEvent: CustomStringConvertible {
577
583
  public var values: [String: Any] {
578
584
  var values = status.values
579
585
  values["type"] = "stream_status"
586
+ if let willRetry {
587
+ values["willRetry"] = willRetry
588
+ }
580
589
  return values
581
590
  }
582
591
 
@@ -111,7 +111,7 @@ class TTSTools {
111
111
  voiceStyle: (modelPath as NSString).appendingPathComponent(kFileVoiceStyle)
112
112
  )
113
113
  var modelConfig = sherpaOnnxOfflineTtsModelConfig(
114
- numThreads: 2,
114
+ numThreads: 8,
115
115
  supertonic: supertonic
116
116
  )
117
117
  var config = sherpaOnnxOfflineTtsConfig(model: modelConfig, maxNumSentences: 1)
@@ -24,6 +24,33 @@ class G1Text {
24
24
 
25
25
  init() {}
26
26
 
27
+ /// Converts text to the base Latin glyphs available in G1 firmware.
28
+ /// Newer glasses bypass this G1-only transport and keep the original text.
29
+ static func sanitizeForDisplay(_ text: String) -> String {
30
+ let expanded = text.reduce(into: "") { result, character in
31
+ switch character {
32
+ case "Đ", "Ð": result.append("D")
33
+ case "đ", "ð": result.append("d")
34
+ case "Ł": result.append("L")
35
+ case "ł": result.append("l")
36
+ case "Ø": result.append("O")
37
+ case "ø": result.append("o")
38
+ case "Æ": result.append("AE")
39
+ case "æ": result.append("ae")
40
+ case "Œ": result.append("OE")
41
+ case "œ": result.append("oe")
42
+ case "ẞ": result.append("SS")
43
+ case "ß": result.append("ss")
44
+ case "Þ": result.append("TH")
45
+ case "þ": result.append("th")
46
+ default: result.append(character)
47
+ }
48
+ }
49
+
50
+ return expanded
51
+ .folding(options: .diacriticInsensitive, locale: Locale(identifier: "en_US_POSIX"))
52
+ }
53
+
27
54
  // MARK: - Text Wall Methods
28
55
 
29
56
  // func displayTextWall(_ text: String) {
@@ -72,7 +99,9 @@ class G1Text {
72
99
  func chunkTextForTransmission(_ text: String) -> [[UInt8]] {
73
100
  // Handle empty or whitespace-only text by sending at least a space
74
101
  // This ensures the display gets updated/cleared properly
75
- let textToSend = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? " " : text
102
+ let textToSend = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
103
+ ? " "
104
+ : G1Text.sanitizeForDisplay(text)
76
105
  guard let textData = textToSend.data(using: .utf8) else { return [] }
77
106
  let textBytes = [UInt8](textData)
78
107
  let totalChunks = Int(ceil(Double(textBytes.count) / Double(G1Text.MAX_CHUNK_SIZE)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.19",
3
+ "version": "0.1.21-beta.0",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -105,7 +105,6 @@
105
105
  "access": "public",
106
106
  "registry": "https://registry.npmjs.org/"
107
107
  },
108
- "dependencies": {},
109
108
  "devDependencies": {
110
109
  "@types/node": "^25.9.3",
111
110
  "expo-module-scripts": "^55.0.2",
@@ -116,6 +116,16 @@ export function isConnectedWifiStatus(status: WifiStatus): status is ConnectedWi
116
116
 
117
117
  export type WifiStatusChangeEvent = WifiStatus & {
118
118
  type: "wifi_status_change"
119
+ /**
120
+ * Glasses-reported provisioning failure reason when THIS event is the verdict of a
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:
123
+ * "connect_timeout" arrives on a disconnected status (never associated), while
124
+ * "connected_to_other_network" arrives on a *connected* status (the attempt failed
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.
127
+ */
128
+ error?: string
119
129
  }
120
130
 
121
131
  export type HotspotStatus = {state: "disabled"} | {state: "enabled"; ssid: string; password: string; localIp: string}
@@ -405,6 +415,7 @@ export type SettingsAckSetting =
405
415
  | "button_video_recording"
406
416
  | "button_max_recording_time"
407
417
  | "camera_fov"
418
+ | "camera_fov_override"
408
419
  | "camera_tuning"
409
420
 
410
421
  export type SettingsAckEvent = {
@@ -416,6 +427,7 @@ export type SettingsAckEvent = {
416
427
  fov?: number
417
428
  roiPosition?: CameraRoiPositionValue
418
429
  hardwareApplied?: boolean
430
+ leaseId?: string
419
431
  active?: boolean
420
432
  size?: ButtonPhotoSize | string
421
433
  width?: number
@@ -440,8 +452,14 @@ export type SettingsAckSuccessEvent = Omit<SettingsAckEvent, "status"> & {
440
452
  export type RgbLedAction = "on" | "off"
441
453
  export type RgbLedColor = "red" | "green" | "blue" | "orange" | "white"
442
454
  export type PhotoSize = "low" | "medium" | "high" | "max"
455
+ export type PhotoMode = "photo" | "text"
443
456
  export type ButtonPhotoSize = "low" | "medium" | "high" | "max"
444
457
 
458
+ /**
459
+ * @deprecated Sticky action-button photo presets via {@link BluetoothSdkPublicModule.setPhotoCaptureDefaults}
460
+ * 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
+ */
445
463
  export type PhotoCaptureDefaults = {
446
464
  size?: PhotoSize
447
465
  mfnr?: boolean
@@ -527,6 +545,13 @@ export type CameraFovResult = {
527
545
  timestamp: number
528
546
  }
529
547
 
548
+ export type CameraFovOverrideRequest = CameraFovRequest & {
549
+ /** Phone-owned lease used to make delayed releases safe. */
550
+ leaseId: string
551
+ /** Safety TTL; refresh the same lease/configuration to extend without a HAL restart. */
552
+ ttlMs?: number
553
+ }
554
+
530
555
  export type CameraFovSetting = {
531
556
  fov: number
532
557
  roiPosition: CameraRoiPositionValue
@@ -544,6 +569,9 @@ export type PhotoRequestParams = {
544
569
  requestId?: string
545
570
  appId?: string
546
571
  size: PhotoSize
572
+ mode?: PhotoMode
573
+ /** `ble` skips direct upload and forces phone-relayed BLE image transfer. */
574
+ transferMethod?: "auto" | "ble"
547
575
  webhookUrl: string | null
548
576
  authToken: string | null
549
577
  compress: PhotoCompression
@@ -566,9 +594,12 @@ export type PhotoRequestParams = {
566
594
  }
567
595
 
568
596
  export type WarmUpCameraParams = {
597
+ /** Supply this when the owner needs to call stopCameraWarmUp during teardown. */
569
598
  requestId?: string
570
599
  size: PhotoSize
600
+ mode?: PhotoMode
571
601
  exposureTimeNs?: number | null
602
+ /** Ready-state hold; defaults to 15 seconds and is capped at 60 seconds by ASG. */
572
603
  durationMs?: number
573
604
  }
574
605
 
@@ -690,11 +721,25 @@ export type StreamResolvedConfig = {
690
721
  }
691
722
  }
692
723
 
724
+ /** Live encoder telemetry, when the glasses emit it (no firmware does yet). */
725
+ export type StreamLiveStats = {
726
+ /** Current encoded video bitrate in bits per second. */
727
+ bitrate?: number
728
+ /** Current encode frame rate. */
729
+ fps?: number
730
+ droppedFrames?: number
731
+ /** Seconds since the stream started. */
732
+ duration?: number
733
+ /** Device temperature in °C, if the hardware reports it. */
734
+ temperatureC?: number
735
+ }
736
+
693
737
  type StreamStatusCommon = {
694
738
  type: "stream_status"
695
739
  streamId?: string
696
740
  timestamp?: number
697
741
  resolvedConfig?: StreamResolvedConfig
742
+ stats?: StreamLiveStats
698
743
  }
699
744
 
700
745
  export type StreamStatusEvent =
@@ -817,6 +862,8 @@ export type BluetoothSdkModuleEvents = {
817
862
  speaking_status: (event: SpeakingStatusEvent) => void
818
863
  battery_status: (event: BatteryStatusEvent) => void
819
864
  local_transcription: (event: LocalTranscriptionEvent) => void
865
+ phone_notification: (event: PhoneNotificationEvent) => void
866
+ phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void
820
867
  wifi_status_change: (event: WifiStatusChangeEvent) => void
821
868
  wifi_scan_result: (event: WifiScanResultEvent) => void
822
869
  hotspot_status_change: (event: HotspotStatusChangeEvent) => void
@@ -863,6 +910,23 @@ export interface ExtractionProgressEvent {
863
910
  totalBytes: number
864
911
  }
865
912
 
913
+ export interface PhoneNotificationEvent {
914
+ notificationId: string
915
+ app: string
916
+ title: string
917
+ content: string
918
+ priority: string
919
+ timestamp: number
920
+ packageName: string
921
+ }
922
+
923
+ export interface PhoneNotificationDismissedEvent {
924
+ notificationId: string
925
+ notificationKey: string
926
+ packageName: string
927
+ timestamp: number
928
+ }
929
+
866
930
  export type PublicGlassesStatus = Omit<
867
931
  GlassesStatus,
868
932
  "otaUpdateAvailable" | "otaProgress" | "otaInProgress" | "otaVersionUrl"
@@ -972,10 +1036,19 @@ export interface BluetoothSdkPublicModule {
972
1036
 
973
1037
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
974
1038
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
1039
+ /**
1040
+ * @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
1042
+ * fields). Still functional until removed in a future release.
1043
+ */
975
1044
  setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): Promise<SettingsAckSuccessEvent>
976
1045
  setVideoRecordingDefaults(settings: VideoRecordingDefaults): Promise<SettingsAckSuccessEvent>
977
1046
  setMaxVideoRecordingDuration(minutes: number): Promise<SettingsAckSuccessEvent>
978
1047
  setCameraFov(request: CameraFovRequest): Promise<CameraFovResult>
1048
+ /** One-way FOV command for legacy ASG clients that do not send settings acknowledgements. */
1049
+ setLegacyCameraFov(request: CameraFovRequest): Promise<CameraFovResult>
1050
+ setCameraFovOverride(request: CameraFovOverrideRequest): Promise<CameraFovResult>
1051
+ releaseCameraFovOverride(leaseId: string): Promise<SettingsAckSuccessEvent>
979
1052
  /**
980
1053
  * Configure camera HAL tuning (ANR / gain) on Mentra Live glasses.
981
1054
  *
@@ -993,6 +1066,8 @@ export interface BluetoothSdkPublicModule {
993
1066
  queryGalleryStatus(): Promise<GalleryStatusEvent>
994
1067
  requestPhoto(params: PhotoRequestParams): Promise<PhotoSuccessResponseEvent>
995
1068
  warmUpCamera(params: WarmUpCameraParams): Promise<CameraStatusEvent>
1069
+ /** Release one request-owned warm-up. Opening requests reject with camera_warm_up_cancelled. */
1070
+ stopCameraWarmUp(requestId: string): Promise<void>
996
1071
  startVideoRecording(
997
1072
  requestId: string,
998
1073
  save: boolean,
package/src/_internal.ts CHANGED
@@ -9,3 +9,5 @@
9
9
  export {default} from "./_private/BluetoothSdkModule"
10
10
  export type {BluetoothSdkInternalModule} from "./_private/BluetoothSdkModule"
11
11
  export * from "./BluetoothSdk.types"
12
+ export {default as MentraLocalNetwork} from "./_private/MentraLocalNetworkModule"
13
+ export * from "./_private/MentraLocalNetworkModule"