@mentra/bluetooth-sdk 0.1.11 → 0.1.13

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 (80) hide show
  1. package/README.md +67 -11
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +73 -6
  5. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +10 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +83 -26
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +266 -31
  9. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +93 -10
  13. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
  14. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
  15. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -13
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
  24. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
  25. package/build/BluetoothSdk.types.d.ts +72 -10
  26. package/build/BluetoothSdk.types.d.ts.map +1 -1
  27. package/build/BluetoothSdk.types.js.map +1 -1
  28. package/build/_private/BluetoothSdkModule.d.ts +9 -7
  29. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  30. package/build/_private/BluetoothSdkModule.js +0 -2
  31. package/build/_private/BluetoothSdkModule.js.map +1 -1
  32. package/build/_private/photoRequestPayload.d.ts +3 -1
  33. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  34. package/build/_private/photoRequestPayload.js +43 -1
  35. package/build/_private/photoRequestPayload.js.map +1 -1
  36. package/build/debug.d.ts +3 -0
  37. package/build/debug.d.ts.map +1 -0
  38. package/build/debug.js +8 -0
  39. package/build/debug.js.map +1 -0
  40. package/build/index.d.ts +1 -1
  41. package/build/index.d.ts.map +1 -1
  42. package/build/index.js +16 -5
  43. package/build/index.js.map +1 -1
  44. package/ios/BluetoothSdkModule.swift +59 -64
  45. package/ios/Source/BluetoothSdkDefaults.swift +47 -0
  46. package/ios/Source/Bridge.swift +6 -0
  47. package/ios/Source/DeviceManager.swift +46 -32
  48. package/ios/Source/DeviceStore.swift +5 -1
  49. package/ios/Source/MentraBluetoothSDK.swift +357 -34
  50. package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
  51. package/ios/Source/ObservableStore.swift +11 -0
  52. package/ios/Source/OtaManifest.swift +170 -0
  53. package/ios/Source/camera/CameraModels.swift +267 -9
  54. package/ios/Source/controllers/ControllerManager.swift +2 -5
  55. package/ios/Source/controllers/R1.swift +2 -5
  56. package/ios/Source/events/BluetoothEvents.swift +3 -0
  57. package/ios/Source/internal/BluetoothAvailability.swift +20 -0
  58. package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
  59. package/ios/Source/sgcs/G1.swift +6 -5
  60. package/ios/Source/sgcs/G2.swift +20 -18
  61. package/ios/Source/sgcs/Mach1.swift +6 -5
  62. package/ios/Source/sgcs/MentraLive.swift +158 -38
  63. package/ios/Source/sgcs/MentraNex.swift +6 -5
  64. package/ios/Source/sgcs/SGCManager.swift +16 -7
  65. package/ios/Source/sgcs/Simulated.swift +7 -3
  66. package/ios/Source/status/DeviceStatus.swift +1 -1
  67. package/ios/Source/types/DeviceModels.swift +5 -1
  68. package/package.json +6 -1
  69. package/plugin/build/index.d.ts +4 -0
  70. package/plugin/build/withAndroid.d.ts +2 -3
  71. package/plugin/build/withAndroid.js +46 -0
  72. package/plugin/build/withIos.d.ts +2 -3
  73. package/plugin/build/withIos.js +32 -0
  74. package/src/BluetoothSdk.types.ts +80 -10
  75. package/src/_private/BluetoothSdkModule.ts +13 -10
  76. package/src/_private/photoRequestPayload.ts +45 -2
  77. package/src/debug.ts +9 -0
  78. package/src/index.ts +25 -6
  79. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
  80. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7405
@@ -18,10 +18,7 @@ protocol SGCManager {
18
18
 
19
19
  // MARK: - Camera & Media
20
20
 
21
- func requestPhoto(
22
- _ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
23
- compress: String?, flash: Bool, save: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
24
- )
21
+ func requestPhoto(_ request: PhotoRequest)
25
22
  func startStream(_ message: [String: Any])
26
23
  func stopStream()
27
24
  func sendStreamKeepAlive(_ message: [String: Any])
@@ -31,9 +28,16 @@ protocol SGCManager {
31
28
  /// Defaulted in an extension to delegate to the basic recording path; devices
32
29
  /// that support custom settings (e.g. Mentra Live) override this.
33
30
  func startVideoRecording(
34
- requestId: String, save: Bool, flash: Bool, sound: Bool, width: Int, height: Int, fps: Int
31
+ requestId: String, save: Bool, flash: Bool, sound: Bool, width: Int, height: Int, fps: Int,
32
+ maxRecordingTimeMinutes: Int
35
33
  )
36
34
  func stopVideoRecording(requestId: String)
35
+ /// Stop recording and upload the result to `webhookUrl` (multipart) using
36
+ /// `authToken`. Supplied at stop time so the token is fresh when the upload
37
+ /// runs. Defaulted in an extension to ignore the upload target and just stop;
38
+ /// devices that support webhook upload (e.g. Mentra Live) override this. An
39
+ /// empty/nil `webhookUrl` means "keep the video on device".
40
+ func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?)
37
41
 
38
42
  // MARK: - Button Settings
39
43
 
@@ -47,6 +51,7 @@ protocol SGCManager {
47
51
 
48
52
  func setBrightness(_ level: Int, autoMode: Bool)
49
53
  func clearDisplay()
54
+ func sendText(_ text: String) async
50
55
  func sendTextWall(_ text: String) async
51
56
  func sendDoubleTextWall(_ top: String, _ bottom: String) async
52
57
  /// Display a bitmap. Optional `x`/`y`/`width`/`height` position and size the target
@@ -111,7 +116,7 @@ protocol SGCManager {
111
116
  func sendWifiCredentials(_ ssid: String, _ password: String)
112
117
  func forgetWifiNetwork(_ ssid: String)
113
118
  func sendHotspotState(_ enabled: Bool)
114
- func sendOtaStart()
119
+ func sendOtaStart(otaVersionUrl: String?)
115
120
  func sendOtaQueryStatus()
116
121
  func sendSetSystemTime(_ timestampMs: Int64)
117
122
  func sendOtaRetryVersionCheck()
@@ -145,11 +150,15 @@ extension SGCManager {
145
150
 
146
151
  func startVideoRecording(
147
152
  requestId: String, save: Bool, flash: Bool, sound: Bool, width _: Int, height _: Int,
148
- fps _: Int
153
+ fps _: Int, maxRecordingTimeMinutes _: Int
149
154
  ) {
150
155
  startVideoRecording(requestId: requestId, save: save, flash: flash, sound: sound)
151
156
  }
152
157
 
158
+ func stopVideoRecording(requestId: String, webhookUrl _: String?, authToken _: String?) {
159
+ stopVideoRecording(requestId: requestId)
160
+ }
161
+
153
162
  // MARK: - Dashboard (default: combined wire format; Nex implements single-field)
154
163
 
155
164
  func setDashboardHeightOnly(_ height: Int) {
@@ -79,8 +79,8 @@ class Simulated: SGCManager {
79
79
 
80
80
  // MARK: - Camera & Media
81
81
 
82
- func requestPhoto(_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?, compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?) {
83
- Bridge.log("requestPhoto")
82
+ func requestPhoto(_ request: PhotoRequest) {
83
+ Bridge.log("requestPhoto flash=\(request.flash) save=\(request.save) sound=\(request.sound)")
84
84
  }
85
85
 
86
86
  func startStream(_: [String: Any]) {
@@ -133,6 +133,10 @@ class Simulated: SGCManager {
133
133
  Bridge.log("clearDisplay")
134
134
  }
135
135
 
136
+ func sendText(_ text: String) async {
137
+ await sendTextWall(text)
138
+ }
139
+
136
140
  func sendTextWall(_: String) async {
137
141
  Bridge.log("sendTextWall")
138
142
  }
@@ -246,7 +250,7 @@ class Simulated: SGCManager {
246
250
  Bridge.log("sendUserEmailToGlasses: \(email)")
247
251
  }
248
252
 
249
- func sendOtaStart() {
253
+ func sendOtaStart(otaVersionUrl: String?) {
250
254
  Bridge.log("sendOtaStart")
251
255
  }
252
256
 
@@ -921,7 +921,7 @@ struct BluetoothStatusUpdate: CustomStringConvertible {
921
921
  }
922
922
 
923
923
  var buttonPhotoSize: ButtonPhotoSize? {
924
- optionalStringValue(values, "button_photo_size").flatMap(ButtonPhotoSize.init(rawValue:))
924
+ optionalStringValue(values, "button_photo_size").map { ButtonPhotoSize(normalizedRawValue: $0) }
925
925
  }
926
926
 
927
927
  var buttonCameraLed: Bool? {
@@ -3,7 +3,11 @@ import Foundation
3
3
  public struct MentraBluetoothSDKConfiguration {
4
4
  public static let `default` = MentraBluetoothSDKConfiguration()
5
5
 
6
- public init() {}
6
+ public let analytics: BluetoothSdkAnalyticsConfiguration
7
+
8
+ public init(analytics: BluetoothSdkAnalyticsConfiguration = BluetoothSdkAnalyticsConfiguration()) {
9
+ self.analytics = analytics
10
+ }
7
11
  }
8
12
 
9
13
  public enum DeviceModel: String {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -22,6 +22,11 @@
22
22
  "react-native": "./src/photo-receiver/index.ts",
23
23
  "default": "./build/photo-receiver/index.js"
24
24
  },
25
+ "./debug": {
26
+ "types": "./build/debug.d.ts",
27
+ "react-native": "./src/debug.ts",
28
+ "default": "./build/debug.js"
29
+ },
25
30
  "./_private/*": null,
26
31
  "./build/*": null,
27
32
  "./src/*": null,
@@ -1,6 +1,10 @@
1
1
  import { type ConfigPlugin } from "expo/config-plugins";
2
2
  export interface BluetoothSdkPluginProps {
3
3
  node?: boolean;
4
+ analytics?: boolean | BluetoothSdkAnalyticsPluginProps;
5
+ }
6
+ export interface BluetoothSdkAnalyticsPluginProps {
7
+ enabled?: boolean;
4
8
  }
5
9
  declare const withBluetoothSdk: ConfigPlugin<BluetoothSdkPluginProps>;
6
10
  export default withBluetoothSdk;
@@ -1,4 +1,3 @@
1
1
  import { type ConfigPlugin } from "expo/config-plugins";
2
- export declare const withAndroidConfiguration: ConfigPlugin<{
3
- node?: boolean;
4
- }>;
2
+ import { type BluetoothSdkPluginProps } from "./index";
3
+ export declare const withAndroidConfiguration: ConfigPlugin<BluetoothSdkPluginProps>;
@@ -7,6 +7,10 @@ exports.withAndroidConfiguration = void 0;
7
7
  const child_process_1 = require("child_process");
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const config_plugins_1 = require("expo/config-plugins");
10
+ const META_ANALYTICS_DISABLED = "com.mentra.bluetoothsdk.analytics.disabled";
11
+ const STALE_META_POSTHOG_API_KEY = "com.mentra.bluetoothsdk.analytics.posthog_api_key";
12
+ const STALE_META_POSTHOG_HOST = "com.mentra.bluetoothsdk.analytics.posthog_host";
13
+ const ANALYTICS_META_NAMES = [META_ANALYTICS_DISABLED, STALE_META_POSTHOG_API_KEY, STALE_META_POSTHOG_HOST];
10
14
  function getBluetoothSdkRoot() {
11
15
  return path_1.default.dirname(require.resolve("../../package.json"));
12
16
  }
@@ -133,9 +137,51 @@ function withSherpaOnnxLocalMavenRepo(config) {
133
137
  return config;
134
138
  });
135
139
  }
140
+ function resolveAnalyticsProps(props) {
141
+ const analytics = props?.analytics;
142
+ let disabled;
143
+ if (analytics === false) {
144
+ disabled = true;
145
+ }
146
+ else if (analytics === true) {
147
+ disabled = false;
148
+ }
149
+ else if (typeof analytics === "object" && analytics.enabled !== undefined) {
150
+ disabled = !analytics.enabled;
151
+ }
152
+ return {
153
+ disabled,
154
+ };
155
+ }
156
+ function upsertMetaData(application, name, value) {
157
+ application["meta-data"] ??= [];
158
+ const existing = application["meta-data"].find((item) => item.$?.["android:name"] === name);
159
+ const entry = existing ?? { $: { "android:name": name } };
160
+ entry.$["android:value"] = value;
161
+ if (!existing) {
162
+ application["meta-data"].push(entry);
163
+ }
164
+ }
165
+ function removeMetaData(application, name) {
166
+ application["meta-data"] = (application["meta-data"] ?? []).filter((item) => item.$?.["android:name"] !== name);
167
+ }
168
+ function withAnalyticsManifestMetadata(config, props) {
169
+ return (0, config_plugins_1.withAndroidManifest)(config, (config) => {
170
+ const analytics = resolveAnalyticsProps(props);
171
+ const application = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
172
+ for (const name of ANALYTICS_META_NAMES) {
173
+ removeMetaData(application, name);
174
+ }
175
+ if (analytics.disabled !== undefined) {
176
+ upsertMetaData(application, META_ANALYTICS_DISABLED, analytics.disabled ? "true" : "false");
177
+ }
178
+ return config;
179
+ });
180
+ }
136
181
  const withAndroidConfiguration = (config, props) => {
137
182
  config = withSettingsGradleModifications(config);
138
183
  config = withSherpaOnnxLocalMavenRepo(config);
184
+ config = withAnalyticsManifestMetadata(config, props);
139
185
  if (props?.node) {
140
186
  config = withGradlePropertiesModifications(config);
141
187
  }
@@ -1,4 +1,3 @@
1
1
  import { type ConfigPlugin } from "expo/config-plugins";
2
- export declare const withIosConfiguration: ConfigPlugin<{
3
- node?: boolean;
4
- }>;
2
+ import { type BluetoothSdkPluginProps } from "./index";
3
+ export declare const withIosConfiguration: ConfigPlugin<BluetoothSdkPluginProps>;
@@ -10,6 +10,9 @@ const path_1 = __importDefault(require("path"));
10
10
  const config_plugins_1 = require("expo/config-plugins");
11
11
  const BLUETOOTH_SDK_EXPO_ADAPTER_ENV = "MENTRA_BLUETOOTH_SDK_INCLUDE_EXPO_ADAPTER";
12
12
  const BLUETOOTH_SDK_EXPO_ADAPTER_LINE = `ENV['${BLUETOOTH_SDK_EXPO_ADAPTER_ENV}'] ||= '1'`;
13
+ const INFO_ANALYTICS_DISABLED = "MentraBluetoothSdkAnalyticsDisabled";
14
+ const STALE_INFO_POSTHOG_API_KEY = "MentraBluetoothSdkPostHogApiKey";
15
+ const STALE_INFO_POSTHOG_HOST = "MentraBluetoothSdkPostHogHost";
13
16
  const ensureBluetoothSdkExpoAdapterPodEnv = (podfile) => {
14
17
  if (podfile.includes(BLUETOOTH_SDK_EXPO_ADAPTER_ENV)) {
15
18
  return podfile;
@@ -51,11 +54,40 @@ const withXcodeEnvLocal = (config) => {
51
54
  },
52
55
  ]);
53
56
  };
57
+ function resolveAnalyticsProps(props) {
58
+ const analytics = props?.analytics;
59
+ let disabled;
60
+ if (analytics === false) {
61
+ disabled = true;
62
+ }
63
+ else if (analytics === true) {
64
+ disabled = false;
65
+ }
66
+ else if (typeof analytics === "object" && analytics.enabled !== undefined) {
67
+ disabled = !analytics.enabled;
68
+ }
69
+ return {
70
+ disabled,
71
+ };
72
+ }
73
+ function withAnalyticsInfoPlist(config, props) {
74
+ return (0, config_plugins_1.withInfoPlist)(config, (config) => {
75
+ const analytics = resolveAnalyticsProps(props);
76
+ delete config.modResults[INFO_ANALYTICS_DISABLED];
77
+ delete config.modResults[STALE_INFO_POSTHOG_API_KEY];
78
+ delete config.modResults[STALE_INFO_POSTHOG_HOST];
79
+ if (analytics.disabled !== undefined) {
80
+ config.modResults[INFO_ANALYTICS_DISABLED] = analytics.disabled;
81
+ }
82
+ return config;
83
+ });
84
+ }
54
85
  const withIosConfiguration = (config, props) => {
55
86
  config = (0, config_plugins_1.withPodfile)(config, (config) => {
56
87
  config.modResults.contents = ensureBluetoothSdkExpoAdapterPodEnv(config.modResults.contents);
57
88
  return config;
58
89
  });
90
+ config = withAnalyticsInfoPlist(config, props);
59
91
  if (props?.node) {
60
92
  config = withXcodeEnvLocal(config);
61
93
  }
@@ -248,6 +248,13 @@ export type PhotoCaptureMetadata = {
248
248
  sensorTimestampNs?: number
249
249
  totalLightProxy?: number
250
250
  mfnrLikely?: boolean
251
+ mfnrApplied?: boolean
252
+ width?: number
253
+ height?: number
254
+ noiseReductionWarning?: "not_implemented" | string
255
+ ispDigitalGainWarning?: "not_implemented" | string
256
+ ispAnalogGainWarning?: "not_implemented" | string
257
+ [key: string]: unknown
251
258
  }
252
259
 
253
260
  export type PhotoStatusEvent = {
@@ -307,6 +314,24 @@ export type VideoRecordingSuccessStatusEvent =
307
314
  | VideoRecordingStartedStatusEvent
308
315
  | VideoRecordingStoppedStatusEvent
309
316
 
317
+ export type MediaUploadSuccessEvent = {
318
+ type: "media_success"
319
+ requestId: string
320
+ mediaUrl: string
321
+ mediaType: number
322
+ timestamp: number
323
+ }
324
+
325
+ export type MediaUploadErrorEvent = {
326
+ type: "media_error"
327
+ requestId: string
328
+ errorMessage: string
329
+ mediaType: number
330
+ timestamp: number
331
+ }
332
+
333
+ export type MediaUploadEvent = MediaUploadSuccessEvent | MediaUploadErrorEvent
334
+
310
335
  export type GalleryStatusEvent = {
311
336
  type: "gallery_status"
312
337
  photos: number
@@ -403,8 +428,24 @@ export type SettingsAckSuccessEvent = Omit<SettingsAckEvent, "status"> & {
403
428
 
404
429
  export type RgbLedAction = "on" | "off"
405
430
  export type RgbLedColor = "red" | "green" | "blue" | "orange" | "white"
406
- export type PhotoSize = "small" | "medium" | "large" | "full"
407
- export type ButtonPhotoSize = "small" | "medium" | "large" | "max"
431
+ export type PhotoSize = "low" | "medium" | "high" | "max"
432
+ export type ButtonPhotoSize = "low" | "medium" | "high" | "max"
433
+
434
+ export type ButtonPhotoSettings = {
435
+ size: ButtonPhotoSize
436
+ mfnr?: boolean
437
+ zsl?: boolean
438
+ noiseReduction?: boolean
439
+ edgeEnhancement?: boolean
440
+ ispDigitalGain?: number
441
+ ispAnalogGain?: string
442
+ aeExposureDivisor?: number
443
+ isoCap?: number
444
+ compress?: PhotoCompression
445
+ sound?: boolean
446
+ /** When true, clears stored NR/edge/ISP presets on the glasses before applying other fields. */
447
+ resetCaptureTuning?: boolean
448
+ }
408
449
  export type PhotoCompression = "none" | "medium" | "heavy"
409
450
 
410
451
  /**
@@ -416,6 +457,12 @@ export interface VideoRecordingSettings {
416
457
  width?: number
417
458
  height?: number
418
459
  fps?: number
460
+ /**
461
+ * Optional auto-stop timer in minutes, sent on `start_video_recording`.
462
+ * `0` (the default) means record until stopped or interrupted
463
+ * (battery/storage/thermal/error).
464
+ */
465
+ maxRecordingTimeMinutes?: number
419
466
  }
420
467
  export const DeviceModels = {
421
468
  Simulated: "Simulated Glasses",
@@ -487,6 +534,17 @@ export type PhotoRequestParams = {
487
534
  exposureTimeNs?: number | null
488
535
  /** Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure. */
489
536
  iso?: number | null
537
+ /** After AE convergence, divide metered exposure by this factor (scan mode). */
538
+ aeExposureDivisor?: number
539
+ /** Cap ISO after AE metering (scan mode). */
540
+ isoCap?: number
541
+ /** Requested on wire; glasses may log not_implemented. */
542
+ noiseReduction?: boolean
543
+ edgeEnhancement?: boolean
544
+ mfnr?: boolean
545
+ zsl?: boolean
546
+ ispDigitalGain?: number
547
+ ispAnalogGain?: string
490
548
  }
491
549
 
492
550
  export type StreamVideoConfig = {
@@ -743,6 +801,8 @@ export type BluetoothSdkModuleEvents = {
743
801
  photo_response: (event: PhotoResponseEvent) => void
744
802
  photo_status: (event: PhotoStatusEvent) => void
745
803
  video_recording_status: (event: VideoRecordingStatusEvent) => void
804
+ media_success: (event: MediaUploadSuccessEvent) => void
805
+ media_error: (event: MediaUploadErrorEvent) => void
746
806
  gallery_status: (event: GalleryStatusEvent) => void
747
807
  compatible_glasses_search_stop: (event: CompatibleGlassesSearchStopEvent) => void
748
808
  heartbeat_sent: (event: HeartbeatSentEvent) => void
@@ -818,6 +878,8 @@ export type BluetoothSdkEventMap = {
818
878
  photo_response: PhotoResponseEvent
819
879
  photo_status: PhotoStatusEvent
820
880
  video_recording_status: VideoRecordingStatusEvent
881
+ media_success: MediaUploadSuccessEvent
882
+ media_error: MediaUploadErrorEvent
821
883
  gallery_status: GalleryStatusEvent
822
884
  compatible_glasses_search_stop: CompatibleGlassesSearchStopEvent
823
885
  swipe_volume_status: SwipeVolumeStatusEvent
@@ -831,7 +893,6 @@ export type BluetoothSdkEventMap = {
831
893
  mic_pcm: MicPcmEvent
832
894
  mic_lc3: MicLc3Event
833
895
  stream_status: StreamStatusEvent
834
- ota_update_available: OtaUpdateAvailableEvent
835
896
  ota_start_ack: OtaStartAckEvent
836
897
  ota_status: OtaStatusEvent
837
898
  version_info: VersionInfoEvent
@@ -885,7 +946,7 @@ export interface BluetoothSdkPublicModule {
885
946
 
886
947
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
887
948
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
888
- setButtonPhotoSettings(size: ButtonPhotoSize): Promise<SettingsAckSuccessEvent>
949
+ setButtonPhotoSettings(settings: ButtonPhotoSettings): Promise<SettingsAckSuccessEvent>
889
950
  setButtonVideoRecordingSettings(width: number, height: number, fps: number): Promise<SettingsAckSuccessEvent>
890
951
  setButtonCameraLed(enabled: boolean): Promise<SettingsAckSuccessEvent>
891
952
  setButtonMaxRecordingTime(minutes: number): Promise<SettingsAckSuccessEvent>
@@ -898,7 +959,18 @@ export interface BluetoothSdkPublicModule {
898
959
  sound: boolean,
899
960
  settings?: VideoRecordingSettings,
900
961
  ): Promise<VideoRecordingStartedStatusEvent>
901
- stopVideoRecording(requestId: string): Promise<VideoRecordingStoppedStatusEvent>
962
+ /**
963
+ * Stop the active recording. When {@link webhookUrl} is provided, the glasses
964
+ * upload the recorded video to it (multipart) using {@link authToken}. These
965
+ * are supplied at stop time (not start) so the token is fresh when the upload
966
+ * runs — a recording can last arbitrarily long. An empty/omitted webhook keeps
967
+ * the video on device (no upload).
968
+ */
969
+ stopVideoRecording(
970
+ requestId: string,
971
+ webhookUrl?: string,
972
+ authToken?: string,
973
+ ): Promise<VideoRecordingStoppedStatusEvent>
902
974
 
903
975
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
904
976
  stopStream(): Promise<StreamStatusEvent>
@@ -920,12 +992,10 @@ export interface BluetoothSdkPublicModule {
920
992
  ): Promise<RgbLedControlSuccessResponseEvent>
921
993
 
922
994
  requestVersionInfo(): Promise<VersionInfoResult>
923
- /** Ask connected Mentra Live glasses to check/report OTA availability and status. */
924
- checkForOtaUpdate(): Promise<OtaQueryResult>
925
- /** Start the OTA flow after your app has presented the available update to the user. */
995
+ /** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
996
+ checkForOtaUpdate(): Promise<boolean>
997
+ /** Start the OTA flow with the same configured manifest URL used by checkForOtaUpdate(). */
926
998
  startOtaUpdate(): Promise<OtaStartAckEvent>
927
- /** Re-run the glasses-side OTA version check, mainly after correcting clock skew/TLS failures. */
928
- retryOtaVersionCheck(): Promise<OtaQueryResult>
929
999
 
930
1000
  // // stt commands (MOVE TO CRUST)
931
1001
  // setSttModelDetails(path: string, languageCode: string): Promise<void>
@@ -2,10 +2,9 @@ import {NativeModule, requireNativeModule} from "expo"
2
2
 
3
3
  import {
4
4
  BluetoothSettingsUpdate,
5
- BluetoothSdkPublicModule,
6
5
  BluetoothSdkModuleEvents,
7
6
  BluetoothStatus,
8
- ButtonPhotoSize,
7
+ ButtonPhotoSettings,
9
8
  CalendarEvent,
10
9
  CAMERA_FOV_DEFAULT,
11
10
  CAMERA_FOV_MAX,
@@ -122,7 +121,8 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
122
121
  // Gallery Commands
123
122
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
124
123
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
125
- setButtonPhotoSettings(size: ButtonPhotoSize): Promise<SettingsAckSuccessEvent>
124
+ setButtonPhotoSettings(settings: ButtonPhotoSettings): Promise<SettingsAckSuccessEvent>
125
+ setButtonPhotoCaptureSettings(settings: ButtonPhotoSettings): Promise<SettingsAckSuccessEvent>
126
126
  setButtonVideoRecordingSettings(width: number, height: number, fps: number): Promise<SettingsAckSuccessEvent>
127
127
  setButtonCameraLed(enabled: boolean): Promise<SettingsAckSuccessEvent>
128
128
  setButtonMaxRecordingTime(minutes: number): Promise<SettingsAckSuccessEvent>
@@ -131,12 +131,13 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
131
131
  requestPhoto(params: PhotoRequestParams): Promise<PhotoSuccessResponseEvent>
132
132
 
133
133
  // OTA Commands
134
- sendOtaStart(): Promise<OtaStartAckEvent>
134
+ setOtaVersionUrl(otaVersionUrl: string): void
135
+ getOtaVersionUrl(): string
136
+ checkForOtaUpdate(): Promise<boolean>
137
+ startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>
135
138
  sendOtaQueryStatus(): Promise<OtaQueryResult>
136
139
  /** Re-run glasses-side OTA version check (called after a clock fix invalidates a TLS failure). */
137
140
  retryOtaVersionCheck(): Promise<OtaQueryResult>
138
- checkForOtaUpdate(): Promise<OtaQueryResult>
139
- startOtaUpdate(): Promise<OtaStartAckEvent>
140
141
 
141
142
  // Version Info Commands
142
143
  requestVersionInfo(): Promise<VersionInfoResult>
@@ -148,7 +149,11 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
148
149
  sound: boolean,
149
150
  settings?: VideoRecordingSettings,
150
151
  ): Promise<VideoRecordingStartedStatusEvent>
151
- stopVideoRecording(requestId: string): Promise<VideoRecordingStoppedStatusEvent>
152
+ stopVideoRecording(
153
+ requestId: string,
154
+ webhookUrl?: string,
155
+ authToken?: string,
156
+ ): Promise<VideoRecordingStoppedStatusEvent>
152
157
 
153
158
  // Stream Commands
154
159
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
@@ -567,8 +572,6 @@ const nativeStartStream = NativeBluetoothSdkModule.startStream.bind(NativeBlueto
567
572
  NativeBluetoothSdkModule.startExternallyManagedStream = function (params: StreamStartRequest) {
568
573
  return nativeStartStream({...params, keepAliveMode: "external"} as StreamStartRequest)
569
574
  }
570
- NativeBluetoothSdkModule.checkForOtaUpdate = NativeBluetoothSdkModule.sendOtaQueryStatus.bind(NativeBluetoothSdkModule)
571
- NativeBluetoothSdkModule.startOtaUpdate = NativeBluetoothSdkModule.sendOtaStart.bind(NativeBluetoothSdkModule)
572
575
 
573
576
  export default NativeBluetoothSdkModule
574
- export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkPublicModule
577
+ export const BluetoothSdk = NativeBluetoothSdkModule as BluetoothSdkInternalModule
@@ -1,4 +1,23 @@
1
- import type {PhotoRequestParams} from "../BluetoothSdk.types"
1
+ import type {PhotoRequestParams, PhotoSize} from "../BluetoothSdk.types"
2
+
3
+ /** Maps unknown/legacy size strings to the current wire format. */
4
+ export function normalizePhotoSizeTier(size: string | undefined): PhotoSize {
5
+ switch (size) {
6
+ case "small":
7
+ return "low"
8
+ case "large":
9
+ return "high"
10
+ case "full":
11
+ return "max"
12
+ case "low":
13
+ case "medium":
14
+ case "high":
15
+ case "max":
16
+ return size
17
+ default:
18
+ return "medium"
19
+ }
20
+ }
2
21
 
3
22
  /** Expo Android bridge rejects null values in Map<String, Any> — omit optional nullish fields. */
4
23
  export function photoRequestParamsForNative(
@@ -7,7 +26,7 @@ export function photoRequestParamsForNative(
7
26
  const payload: Record<string, string | number | boolean> = {
8
27
  requestId: params.requestId,
9
28
  appId: params.appId,
10
- size: params.size,
29
+ size: normalizePhotoSizeTier(params.size),
11
30
  webhookUrl: params.webhookUrl ?? "",
12
31
  compress: params.compress,
13
32
  flash: true,
@@ -24,5 +43,29 @@ export function photoRequestParamsForNative(
24
43
  if (hasManualExposure && params.iso != null && Number.isFinite(params.iso) && params.iso > 0) {
25
44
  payload.iso = Math.round(params.iso)
26
45
  }
46
+ if (params.aeExposureDivisor != null && params.aeExposureDivisor > 1) {
47
+ payload.aeExposureDivisor = Math.round(params.aeExposureDivisor)
48
+ }
49
+ if (params.isoCap != null && params.isoCap > 0) {
50
+ payload.isoCap = Math.round(params.isoCap)
51
+ }
52
+ if (params.noiseReduction != null) {
53
+ payload.noiseReduction = params.noiseReduction
54
+ }
55
+ if (params.edgeEnhancement != null) {
56
+ payload.edgeEnhancement = params.edgeEnhancement
57
+ }
58
+ if (params.mfnr != null) {
59
+ payload.mfnr = params.mfnr
60
+ }
61
+ if (params.zsl != null) {
62
+ payload.zsl = params.zsl
63
+ }
64
+ if (params.ispDigitalGain != null) {
65
+ payload.ispDigitalGain = Math.round(params.ispDigitalGain)
66
+ }
67
+ if (params.ispAnalogGain != null && params.ispAnalogGain.length > 0) {
68
+ payload.ispAnalogGain = params.ispAnalogGain
69
+ }
27
70
  return payload
28
71
  }
package/src/debug.ts ADDED
@@ -0,0 +1,9 @@
1
+ import PrivateBluetoothSdkModule from "./_private/BluetoothSdkModule"
2
+
3
+ export function setOtaVersionUrl(otaVersionUrl: string): void {
4
+ PrivateBluetoothSdkModule.setOtaVersionUrl(otaVersionUrl)
5
+ }
6
+
7
+ export function getOtaVersionUrl(): string {
8
+ return PrivateBluetoothSdkModule.getOtaVersionUrl()
9
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import PrivateBluetoothSdkModule from "./_private/BluetoothSdkModule"
2
- import type {BluetoothSdkEventListener, BluetoothSdkEventName, BluetoothSdkPublicModule} from "./BluetoothSdk.types"
2
+ import type {
3
+ BluetoothSdkEventListener,
4
+ BluetoothSdkEventName,
5
+ BluetoothSdkPublicModule,
6
+ ButtonPhotoSettings,
7
+ } from "./BluetoothSdk.types"
3
8
 
4
9
  const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
5
10
  "log",
@@ -21,6 +26,8 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
21
26
  "photo_response",
22
27
  "photo_status",
23
28
  "video_recording_status",
29
+ "media_success",
30
+ "media_error",
24
31
  "gallery_status",
25
32
  "compatible_glasses_search_stop",
26
33
  "swipe_volume_status",
@@ -34,7 +41,6 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
34
41
  "mic_pcm",
35
42
  "mic_lc3",
36
43
  "stream_status",
37
- "ota_update_available",
38
44
  "ota_start_ack",
39
45
  "ota_status",
40
46
  "version_info",
@@ -48,6 +54,10 @@ const addListener: BluetoothSdkPublicModule["addListener"] = (eventName, listene
48
54
  return PrivateBluetoothSdkModule.addListener(eventName, listener as BluetoothSdkEventListener<BluetoothSdkEventName>)
49
55
  }
50
56
 
57
+ const startOtaUpdate: BluetoothSdkPublicModule["startOtaUpdate"] = () => {
58
+ return PrivateBluetoothSdkModule.startOtaUpdate()
59
+ }
60
+
51
61
  export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
52
62
  addListener,
53
63
  getDefaultDevice: PrivateBluetoothSdkModule.getDefaultDevice.bind(PrivateBluetoothSdkModule),
@@ -75,7 +85,15 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
75
85
  setGalleryModeEnabled: PrivateBluetoothSdkModule.setGalleryModeEnabled.bind(PrivateBluetoothSdkModule),
76
86
  setVoiceActivityDetectionEnabled:
77
87
  PrivateBluetoothSdkModule.setVoiceActivityDetectionEnabled.bind(PrivateBluetoothSdkModule),
78
- setButtonPhotoSettings: PrivateBluetoothSdkModule.setButtonPhotoSettings.bind(PrivateBluetoothSdkModule),
88
+ setButtonPhotoSettings: (settings: ButtonPhotoSettings) => {
89
+ // setButtonPhotoCaptureSettings is available in SDK 0.1.13+. Guard for OTA version-skew
90
+ // where a new JS bundle runs against an older native module that only has the string form.
91
+ if (typeof PrivateBluetoothSdkModule.setButtonPhotoCaptureSettings === "function") {
92
+ return PrivateBluetoothSdkModule.setButtonPhotoCaptureSettings(settings)
93
+ }
94
+ // Legacy fallback: old native bridge only accepts a size string
95
+ return PrivateBluetoothSdkModule.setButtonPhotoSettings({size: settings.size ?? "max"} as any)
96
+ },
79
97
  setButtonVideoRecordingSettings:
80
98
  PrivateBluetoothSdkModule.setButtonVideoRecordingSettings.bind(PrivateBluetoothSdkModule),
81
99
  setButtonCameraLed: PrivateBluetoothSdkModule.setButtonCameraLed.bind(PrivateBluetoothSdkModule),
@@ -94,9 +112,8 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
94
112
  setGlassesMediaVolume: PrivateBluetoothSdkModule.setGlassesMediaVolume.bind(PrivateBluetoothSdkModule),
95
113
  rgbLedControl: PrivateBluetoothSdkModule.rgbLedControl.bind(PrivateBluetoothSdkModule),
96
114
  requestVersionInfo: PrivateBluetoothSdkModule.requestVersionInfo.bind(PrivateBluetoothSdkModule),
97
- checkForOtaUpdate: PrivateBluetoothSdkModule.sendOtaQueryStatus.bind(PrivateBluetoothSdkModule),
98
- startOtaUpdate: PrivateBluetoothSdkModule.sendOtaStart.bind(PrivateBluetoothSdkModule),
99
- retryOtaVersionCheck: PrivateBluetoothSdkModule.retryOtaVersionCheck.bind(PrivateBluetoothSdkModule),
115
+ checkForOtaUpdate: PrivateBluetoothSdkModule.checkForOtaUpdate.bind(PrivateBluetoothSdkModule),
116
+ startOtaUpdate,
100
117
  setSttModelDetails: PrivateBluetoothSdkModule.setSttModelDetails.bind(PrivateBluetoothSdkModule),
101
118
  getSttModelPath: PrivateBluetoothSdkModule.getSttModelPath.bind(PrivateBluetoothSdkModule),
102
119
  checkSttModelAvailable: PrivateBluetoothSdkModule.checkSttModelAvailable.bind(PrivateBluetoothSdkModule),
@@ -138,6 +155,7 @@ export type {
138
155
  BluetoothSdkPublicModule as BluetoothSdkModule,
139
156
  BluetoothSdkSubscription,
140
157
  ButtonPhotoSize,
158
+ ButtonPhotoSettings,
141
159
  ButtonPressEvent,
142
160
  CameraFovPreset,
143
161
  CameraFovRequest,
@@ -179,6 +197,7 @@ export type {
179
197
  PhotoMeteredPreview,
180
198
  PhotoResponseEvent,
181
199
  PhotoRequestedCaptureConfig,
200
+ PhotoRequestParams,
182
201
  PhotoSize,
183
202
  PhotoStatusEvent,
184
203
  PhotoStatusState,