@mentra/bluetooth-sdk 0.1.20 → 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 (73) hide show
  1. package/README.md +5 -1
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +127 -24
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +46 -3
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +369 -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 +3 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +183 -30
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +4 -2
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
  17. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
  18. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +15 -2
  19. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
  20. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  21. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +15 -0
  22. package/build/BluetoothSdk.types.d.ts +46 -1
  23. package/build/BluetoothSdk.types.d.ts.map +1 -1
  24. package/build/BluetoothSdk.types.js.map +1 -1
  25. package/build/_internal.d.ts +2 -0
  26. package/build/_internal.d.ts.map +1 -1
  27. package/build/_internal.js +2 -0
  28. package/build/_internal.js.map +1 -1
  29. package/build/_private/BluetoothSdkModule.d.ts +23 -2
  30. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  31. package/build/_private/BluetoothSdkModule.js +20 -0
  32. package/build/_private/BluetoothSdkModule.js.map +1 -1
  33. package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
  34. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
  35. package/build/_private/MentraLocalNetworkModule.js +3 -0
  36. package/build/_private/MentraLocalNetworkModule.js.map +1 -0
  37. package/build/_private/cameraRequestPayload.d.ts.map +1 -1
  38. package/build/_private/cameraRequestPayload.js +1 -0
  39. package/build/_private/cameraRequestPayload.js.map +1 -1
  40. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  41. package/build/_private/photoRequestPayload.js +1 -0
  42. package/build/_private/photoRequestPayload.js.map +1 -1
  43. package/build/index.d.ts.map +1 -1
  44. package/build/index.js +4 -0
  45. package/build/index.js.map +1 -1
  46. package/expo-module.config.json +1 -0
  47. package/ios/BluetoothSdkModule.swift +72 -0
  48. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  49. package/ios/Source/Bridge.swift +72 -18
  50. package/ios/Source/DeviceManager.swift +51 -3
  51. package/ios/Source/DeviceStore.swift +1 -1
  52. package/ios/Source/MentraBluetoothSDK.swift +289 -25
  53. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  54. package/ios/Source/camera/CameraModels.swift +8 -3
  55. package/ios/Source/sgcs/G1.swift +1 -1
  56. package/ios/Source/sgcs/G2.swift +19 -7
  57. package/ios/Source/sgcs/Mach1.swift +1 -1
  58. package/ios/Source/sgcs/MentraLive.swift +318 -37
  59. package/ios/Source/sgcs/MentraNex.swift +1 -1
  60. package/ios/Source/sgcs/Nimo.swift +1 -1
  61. package/ios/Source/sgcs/SGCManager.swift +5 -2
  62. package/ios/Source/sgcs/Simulated.swift +1 -1
  63. package/ios/Source/status/WifiHotspotStatus.swift +21 -2
  64. package/ios/Source/streaming/StreamModels.swift +9 -0
  65. package/ios/Source/tts/TTSTools.swift +1 -1
  66. package/package.json +1 -2
  67. package/src/BluetoothSdk.types.ts +49 -0
  68. package/src/_internal.ts +2 -0
  69. package/src/_private/BluetoothSdkModule.ts +64 -13
  70. package/src/_private/MentraLocalNetworkModule.ts +50 -0
  71. package/src/_private/cameraRequestPayload.ts +1 -0
  72. package/src/_private/photoRequestPayload.ts +2 -3
  73. package/src/index.ts +4 -0
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.20",
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
@@ -533,6 +545,13 @@ export type CameraFovResult = {
533
545
  timestamp: number
534
546
  }
535
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
+
536
555
  export type CameraFovSetting = {
537
556
  fov: number
538
557
  roiPosition: CameraRoiPositionValue
@@ -551,6 +570,8 @@ export type PhotoRequestParams = {
551
570
  appId?: string
552
571
  size: PhotoSize
553
572
  mode?: PhotoMode
573
+ /** `ble` skips direct upload and forces phone-relayed BLE image transfer. */
574
+ transferMethod?: "auto" | "ble"
554
575
  webhookUrl: string | null
555
576
  authToken: string | null
556
577
  compress: PhotoCompression
@@ -573,9 +594,12 @@ export type PhotoRequestParams = {
573
594
  }
574
595
 
575
596
  export type WarmUpCameraParams = {
597
+ /** Supply this when the owner needs to call stopCameraWarmUp during teardown. */
576
598
  requestId?: string
577
599
  size: PhotoSize
600
+ mode?: PhotoMode
578
601
  exposureTimeNs?: number | null
602
+ /** Ready-state hold; defaults to 15 seconds and is capped at 60 seconds by ASG. */
579
603
  durationMs?: number
580
604
  }
581
605
 
@@ -838,6 +862,8 @@ export type BluetoothSdkModuleEvents = {
838
862
  speaking_status: (event: SpeakingStatusEvent) => void
839
863
  battery_status: (event: BatteryStatusEvent) => void
840
864
  local_transcription: (event: LocalTranscriptionEvent) => void
865
+ phone_notification: (event: PhoneNotificationEvent) => void
866
+ phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void
841
867
  wifi_status_change: (event: WifiStatusChangeEvent) => void
842
868
  wifi_scan_result: (event: WifiScanResultEvent) => void
843
869
  hotspot_status_change: (event: HotspotStatusChangeEvent) => void
@@ -884,6 +910,23 @@ export interface ExtractionProgressEvent {
884
910
  totalBytes: number
885
911
  }
886
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
+
887
930
  export type PublicGlassesStatus = Omit<
888
931
  GlassesStatus,
889
932
  "otaUpdateAvailable" | "otaProgress" | "otaInProgress" | "otaVersionUrl"
@@ -1002,6 +1045,10 @@ export interface BluetoothSdkPublicModule {
1002
1045
  setVideoRecordingDefaults(settings: VideoRecordingDefaults): Promise<SettingsAckSuccessEvent>
1003
1046
  setMaxVideoRecordingDuration(minutes: number): Promise<SettingsAckSuccessEvent>
1004
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>
1005
1052
  /**
1006
1053
  * Configure camera HAL tuning (ANR / gain) on Mentra Live glasses.
1007
1054
  *
@@ -1019,6 +1066,8 @@ export interface BluetoothSdkPublicModule {
1019
1066
  queryGalleryStatus(): Promise<GalleryStatusEvent>
1020
1067
  requestPhoto(params: PhotoRequestParams): Promise<PhotoSuccessResponseEvent>
1021
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>
1022
1071
  startVideoRecording(
1023
1072
  requestId: string,
1024
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"
@@ -11,6 +11,7 @@ import {
11
11
  CAMERA_FOV_MIN,
12
12
  CameraFovPreset,
13
13
  CameraFovRequest,
14
+ CameraFovOverrideRequest,
14
15
  CameraFovResult,
15
16
  CameraFovSetting,
16
17
  CameraRoiPosition,
@@ -116,7 +117,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
116
117
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
117
118
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>
118
119
  setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
119
- /** Set glasses system clock (Mentra Live only) when phone detects clock skew. */
120
+ /** Set the glasses system clock when phone detects clock skew (Mentra Live and G2). */
120
121
  setSystemTime(timestampMs: number): Promise<void>
121
122
  /** Logs current WiFi frequency (MHz) and 5 GHz band to Android logcat. */
122
123
  logCurrentWifiFrequency(): Promise<void>
@@ -132,6 +133,10 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
132
133
  setVideoRecordingDefaults(width: number, height: number, fps: number): Promise<SettingsAckSuccessEvent>
133
134
  setMaxVideoRecordingDuration(minutes: number): Promise<SettingsAckSuccessEvent>
134
135
  setCameraFov(request: CameraFovRequest): Promise<CameraFovResult>
136
+ setLegacyCameraFov(request: CameraFovRequest): Promise<CameraFovResult>
137
+ restoreLegacyCameraFov(): Promise<void>
138
+ setCameraFovOverride(request: CameraFovOverrideRequest): Promise<CameraFovResult>
139
+ releaseCameraFovOverride(leaseId: string): Promise<SettingsAckSuccessEvent>
135
140
  /**
136
141
  * Configure camera HAL tuning (ANR / gain) on Mentra Live glasses.
137
142
  * Sends a {@code camera_tuning_config} BLE message; the ASG client relays it as a
@@ -144,6 +149,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
144
149
  queryGalleryStatus(): Promise<GalleryStatusEvent>
145
150
  requestPhoto(params: PhotoRequestParams): Promise<PhotoSuccessResponseEvent>
146
151
  warmUpCamera(params: WarmUpCameraParams): Promise<CameraStatusEvent>
152
+ stopCameraWarmUp(requestId: string): Promise<void>
147
153
 
148
154
  // OTA Commands
149
155
  setOtaVersionUrl(otaVersionUrl: string): void
@@ -184,6 +190,23 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
184
190
  // Used to suspend LC3 mic during audio playback to avoid MCU overload
185
191
  setOwnAppAudioPlaying(playing: boolean): Promise<void>
186
192
 
193
+ // Live PCM output stream (miniapp speaker.createStream). MentraOS-internal
194
+ // — 16-bit LE PCM chunks into a streaming AudioTrack (USAGE_MEDIA, so it
195
+ // follows the phone's media route, e.g. A2DP to glasses). Implemented with
196
+ // AudioTrack on Android and AVAudioEngine on iOS.
197
+ /** Open a PCM stream session. One AudioTrack per id; caller manages ids. */
198
+ pcmStreamOpen(streamId: string, sampleRate: number, channels: number, volume: number): Promise<void>
199
+ /**
200
+ * Append base64 PCM. Resolves with the queued-but-unplayed backlog in ms;
201
+ * blocks (on a background dispatcher) while the backlog is above the
202
+ * backpressure ceiling, so awaited writes self-throttle to realtime.
203
+ */
204
+ pcmStreamWrite(streamId: string, base64: string): Promise<{bufferedMs: number}>
205
+ /** Drain the backlog, then stop. Resolves with the total played duration. */
206
+ pcmStreamClose(streamId: string): Promise<{durationMs: number}>
207
+ /** Stop immediately, dropping any backlog. Idempotent. */
208
+ pcmStreamAbort(streamId: string): Promise<void>
209
+
187
210
  /** Mentra Live only: K900 `cs_getvol` / `sr_getvol`. */
188
211
  getGlassesMediaVolume(): Promise<GlassesMediaVolumeGetResult>
189
212
  /** Mentra Live only: K900 `cs_vol` / `sr_vol`; level clamped 0–15 on native. */
@@ -244,10 +267,7 @@ const DEFAULT_CONNECT_OPTIONS: Required<ConnectOptions> = {
244
267
 
245
268
  const DEFAULT_SCAN_TIMEOUT_MS = 15_000
246
269
 
247
- function bindNativeMethod<T extends (...args: never[]) => unknown>(
248
- module: Record<string, unknown>,
249
- name: string,
250
- ): T {
270
+ function bindNativeMethod<T extends (...args: never[]) => unknown>(module: Record<string, unknown>, name: string): T {
251
271
  const method = module[name]
252
272
  if (typeof method !== "function") {
253
273
  console.warn(`[BluetoothSdk] Native method "${name}" is unavailable — rebuild the app (bun android / bun ios)`)
@@ -286,11 +306,7 @@ function normalizeCameraFov(request: CameraFovRequest): CameraFovSetting {
286
306
  const roiPosition = request.roiPosition ?? "center"
287
307
 
288
308
  return {
289
- fov: clampInteger(
290
- Number.isFinite(request.fov) ? request.fov : CAMERA_FOV_DEFAULT,
291
- CAMERA_FOV_MIN,
292
- CAMERA_FOV_MAX,
293
- ),
309
+ fov: clampInteger(Number.isFinite(request.fov) ? request.fov : CAMERA_FOV_DEFAULT, CAMERA_FOV_MIN, CAMERA_FOV_MAX),
294
310
  roiPosition: clampInteger(CAMERA_ROI_POSITION_VALUES[roiPosition] ?? 0, CAMERA_ROI_MIN, CAMERA_ROI_MAX) as
295
311
  | 0
296
312
  | 1
@@ -470,14 +486,49 @@ NativeBluetoothSdkModule.setVoiceActivityDetectionEnabled = function (enabled: b
470
486
  return this.updateBluetoothSettings({voice_activity_detection_enabled: enabled})
471
487
  }
472
488
 
473
- const nativeSetCameraFov = bindNativeMethod<
474
- (fov: CameraFovSetting) => MaybePromise<CameraFovResult>
475
- >(NativeBluetoothSdkModule as unknown as Record<string, unknown>, "setCameraFov")
489
+ const nativeSetCameraFov = bindNativeMethod<(fov: CameraFovSetting) => MaybePromise<CameraFovResult>>(
490
+ NativeBluetoothSdkModule as unknown as Record<string, unknown>,
491
+ "setCameraFov",
492
+ )
476
493
  NativeBluetoothSdkModule.setCameraFov = function (request: CameraFovRequest) {
477
494
  const setting = normalizeCameraFov(request)
478
495
  return Promise.resolve(nativeSetCameraFov(setting))
479
496
  }
480
497
 
498
+ const nativeSetLegacyCameraFov = bindNativeMethod<(fov: CameraFovSetting) => MaybePromise<CameraFovResult>>(
499
+ NativeBluetoothSdkModule as unknown as Record<string, unknown>,
500
+ "setLegacyCameraFov",
501
+ )
502
+ NativeBluetoothSdkModule.setLegacyCameraFov = function (request: CameraFovRequest) {
503
+ return Promise.resolve(nativeSetLegacyCameraFov(normalizeCameraFov(request)))
504
+ }
505
+
506
+ const nativeRestoreLegacyCameraFov = bindNativeMethod<() => MaybePromise<void>>(
507
+ NativeBluetoothSdkModule as unknown as Record<string, unknown>,
508
+ "restoreLegacyCameraFov",
509
+ )
510
+ NativeBluetoothSdkModule.restoreLegacyCameraFov = function () {
511
+ return Promise.resolve(nativeRestoreLegacyCameraFov())
512
+ }
513
+
514
+ const nativeSetCameraFovOverride = bindNativeMethod<
515
+ (request: CameraFovSetting & {leaseId: string; ttlMs: number}) => MaybePromise<CameraFovResult>
516
+ >(NativeBluetoothSdkModule as unknown as Record<string, unknown>, "setCameraFovOverride")
517
+ NativeBluetoothSdkModule.setCameraFovOverride = function (request: CameraFovOverrideRequest) {
518
+ const setting = normalizeCameraFov(request)
519
+ const requestedTtl =
520
+ typeof request.ttlMs === "number" && Number.isFinite(request.ttlMs) && request.ttlMs > 0
521
+ ? Math.round(request.ttlMs)
522
+ : 300_000
523
+ return Promise.resolve(
524
+ nativeSetCameraFovOverride({
525
+ ...setting,
526
+ leaseId: request.leaseId,
527
+ ttlMs: Math.min(requestedTtl, 600_000),
528
+ }),
529
+ )
530
+ }
531
+
481
532
  NativeBluetoothSdkModule.setMicState = function (
482
533
  enabled: boolean,
483
534
  useGlassesMic?: boolean,
@@ -0,0 +1,50 @@
1
+ import {NativeModule, requireOptionalNativeModule} from "expo"
2
+
3
+ export type LocalNetworkResponse = {
4
+ status: number
5
+ headers: Record<string, string>
6
+ bodyBase64: string
7
+ }
8
+
9
+ export type LocalNetworkDownloadResult = {
10
+ statusCode: number
11
+ bytesWritten: number
12
+ headers: Record<string, string>
13
+ }
14
+
15
+ export type LocalNetworkDownloadProgress = {
16
+ requestId: string
17
+ bytesWritten: number
18
+ contentLength: number
19
+ statusCode?: number
20
+ headers?: Record<string, string>
21
+ }
22
+
23
+ type LocalNetworkEvents = {
24
+ downloadProgress: (event: LocalNetworkDownloadProgress) => void
25
+ networkLost: (event: {ssid: string}) => void
26
+ }
27
+
28
+ declare class MentraLocalNetworkNativeModule extends NativeModule<LocalNetworkEvents> {
29
+ connect(ssid: string, password: string): Promise<{connected: boolean; ssid: string}>
30
+ request(
31
+ requestId: string,
32
+ url: string,
33
+ method: string,
34
+ headers: Record<string, string>,
35
+ body: string | null,
36
+ timeoutMs: number,
37
+ ): Promise<LocalNetworkResponse>
38
+ download(
39
+ requestId: string,
40
+ url: string,
41
+ destination: string,
42
+ headers: Record<string, string>,
43
+ connectionTimeoutMs: number,
44
+ readTimeoutMs: number,
45
+ ): Promise<LocalNetworkDownloadResult>
46
+ cancel(requestId: string): Promise<void>
47
+ disconnect(): Promise<void>
48
+ }
49
+
50
+ export default requireOptionalNativeModule<MentraLocalNetworkNativeModule>("MentraLocalNetwork")
@@ -12,6 +12,7 @@ export function warmUpCameraParamsForNative(
12
12
  ): Record<string, string | number> {
13
13
  const payload: Record<string, string | number> = {
14
14
  size: normalizePhotoSizeTier(params.size),
15
+ mode: params.mode ?? "photo",
15
16
  }
16
17
  const requestId = nonBlankString(params.requestId)
17
18
  if (requestId != null) {
@@ -25,12 +25,11 @@ function nonBlankString(value?: string | null): string | undefined {
25
25
  }
26
26
 
27
27
  /** Expo Android bridge rejects null values in Map<String, Any> — omit optional nullish fields. */
28
- export function photoRequestParamsForNative(
29
- params: PhotoRequestParams,
30
- ): Record<string, string | number | boolean> {
28
+ export function photoRequestParamsForNative(params: PhotoRequestParams): Record<string, string | number | boolean> {
31
29
  const payload: Record<string, string | number | boolean> = {
32
30
  size: normalizePhotoSizeTier(params.size),
33
31
  mode: params.mode ?? "photo",
32
+ transferMethod: params.transferMethod ?? "auto",
34
33
  webhookUrl: params.webhookUrl ?? "",
35
34
  compress: params.compress,
36
35
  sound: params.sound,
package/src/index.ts CHANGED
@@ -113,10 +113,14 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
113
113
  },
114
114
  setMaxVideoRecordingDuration: bindPublicMethod("setMaxVideoRecordingDuration"),
115
115
  setCameraFov: bindPublicMethod("setCameraFov"),
116
+ setLegacyCameraFov: bindPublicMethod("setLegacyCameraFov"),
117
+ setCameraFovOverride: bindPublicMethod("setCameraFovOverride"),
118
+ releaseCameraFovOverride: bindPublicMethod("releaseCameraFovOverride"),
116
119
  setCameraTuningConfig: bindPublicMethod("setCameraTuningConfig"),
117
120
  queryGalleryStatus: bindPublicMethod("queryGalleryStatus"),
118
121
  requestPhoto: bindPublicMethod("requestPhoto"),
119
122
  warmUpCamera: bindPublicMethod("warmUpCamera"),
123
+ stopCameraWarmUp: bindPublicMethod("stopCameraWarmUp"),
120
124
  startVideoRecording: bindPublicMethod("startVideoRecording"),
121
125
  stopVideoRecording: bindPublicMethod("stopVideoRecording"),
122
126
  startStream: bindPublicMethod("startStream"),