@mentra/bluetooth-sdk 0.1.11 → 0.1.12

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 (42) hide show
  1. package/README.md +52 -3
  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 +22 -3
  5. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +10 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +10 -5
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +76 -5
  8. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +15 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +34 -5
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +12 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
  13. package/build/BluetoothSdk.types.d.ts +33 -1
  14. package/build/BluetoothSdk.types.d.ts.map +1 -1
  15. package/build/BluetoothSdk.types.js.map +1 -1
  16. package/build/_private/BluetoothSdkModule.d.ts +1 -1
  17. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  18. package/build/_private/BluetoothSdkModule.js.map +1 -1
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js +2 -0
  21. package/build/index.js.map +1 -1
  22. package/ios/BluetoothSdkModule.swift +21 -7
  23. package/ios/Source/BluetoothSdkDefaults.swift +27 -0
  24. package/ios/Source/Bridge.swift +6 -0
  25. package/ios/Source/DeviceManager.swift +9 -6
  26. package/ios/Source/MentraBluetoothSDK.swift +160 -8
  27. package/ios/Source/camera/CameraModels.swift +49 -1
  28. package/ios/Source/events/BluetoothEvents.swift +3 -0
  29. package/ios/Source/internal/BluetoothAvailability.swift +20 -0
  30. package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
  31. package/ios/Source/sgcs/MentraLive.swift +29 -5
  32. package/ios/Source/sgcs/SGCManager.swift +13 -2
  33. package/ios/Source/types/DeviceModels.swift +5 -1
  34. package/package.json +1 -1
  35. package/plugin/build/index.d.ts +4 -0
  36. package/plugin/build/withAndroid.d.ts +2 -3
  37. package/plugin/build/withAndroid.js +46 -0
  38. package/plugin/build/withIos.d.ts +2 -3
  39. package/plugin/build/withIos.js +32 -0
  40. package/src/BluetoothSdk.types.ts +40 -1
  41. package/src/_private/BluetoothSdkModule.ts +5 -1
  42. package/src/index.ts +2 -0
@@ -31,9 +31,16 @@ protocol SGCManager {
31
31
  /// Defaulted in an extension to delegate to the basic recording path; devices
32
32
  /// that support custom settings (e.g. Mentra Live) override this.
33
33
  func startVideoRecording(
34
- requestId: String, save: Bool, flash: Bool, sound: Bool, width: Int, height: Int, fps: Int
34
+ requestId: String, save: Bool, flash: Bool, sound: Bool, width: Int, height: Int, fps: Int,
35
+ maxRecordingTimeMinutes: Int
35
36
  )
36
37
  func stopVideoRecording(requestId: String)
38
+ /// Stop recording and upload the result to `webhookUrl` (multipart) using
39
+ /// `authToken`. Supplied at stop time so the token is fresh when the upload
40
+ /// runs. Defaulted in an extension to ignore the upload target and just stop;
41
+ /// devices that support webhook upload (e.g. Mentra Live) override this. An
42
+ /// empty/nil `webhookUrl` means "keep the video on device".
43
+ func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?)
37
44
 
38
45
  // MARK: - Button Settings
39
46
 
@@ -145,11 +152,15 @@ extension SGCManager {
145
152
 
146
153
  func startVideoRecording(
147
154
  requestId: String, save: Bool, flash: Bool, sound: Bool, width _: Int, height _: Int,
148
- fps _: Int
155
+ fps _: Int, maxRecordingTimeMinutes _: Int
149
156
  ) {
150
157
  startVideoRecording(requestId: requestId, save: save, flash: flash, sound: sound)
151
158
  }
152
159
 
160
+ func stopVideoRecording(requestId: String, webhookUrl _: String?, authToken _: String?) {
161
+ stopVideoRecording(requestId: requestId)
162
+ }
163
+
153
164
  // MARK: - Dashboard (default: combined wire format; Nex implements single-field)
154
165
 
155
166
  func setDashboardHeightOnly(_ height: Int) {
@@ -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.12",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -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
  }
@@ -307,6 +307,24 @@ export type VideoRecordingSuccessStatusEvent =
307
307
  | VideoRecordingStartedStatusEvent
308
308
  | VideoRecordingStoppedStatusEvent
309
309
 
310
+ export type MediaUploadSuccessEvent = {
311
+ type: "media_success"
312
+ requestId: string
313
+ mediaUrl: string
314
+ mediaType: number
315
+ timestamp: number
316
+ }
317
+
318
+ export type MediaUploadErrorEvent = {
319
+ type: "media_error"
320
+ requestId: string
321
+ errorMessage: string
322
+ mediaType: number
323
+ timestamp: number
324
+ }
325
+
326
+ export type MediaUploadEvent = MediaUploadSuccessEvent | MediaUploadErrorEvent
327
+
310
328
  export type GalleryStatusEvent = {
311
329
  type: "gallery_status"
312
330
  photos: number
@@ -416,6 +434,12 @@ export interface VideoRecordingSettings {
416
434
  width?: number
417
435
  height?: number
418
436
  fps?: number
437
+ /**
438
+ * Optional auto-stop timer in minutes, sent on `start_video_recording`.
439
+ * `0` (the default) means record until stopped or interrupted
440
+ * (battery/storage/thermal/error).
441
+ */
442
+ maxRecordingTimeMinutes?: number
419
443
  }
420
444
  export const DeviceModels = {
421
445
  Simulated: "Simulated Glasses",
@@ -743,6 +767,8 @@ export type BluetoothSdkModuleEvents = {
743
767
  photo_response: (event: PhotoResponseEvent) => void
744
768
  photo_status: (event: PhotoStatusEvent) => void
745
769
  video_recording_status: (event: VideoRecordingStatusEvent) => void
770
+ media_success: (event: MediaUploadSuccessEvent) => void
771
+ media_error: (event: MediaUploadErrorEvent) => void
746
772
  gallery_status: (event: GalleryStatusEvent) => void
747
773
  compatible_glasses_search_stop: (event: CompatibleGlassesSearchStopEvent) => void
748
774
  heartbeat_sent: (event: HeartbeatSentEvent) => void
@@ -818,6 +844,8 @@ export type BluetoothSdkEventMap = {
818
844
  photo_response: PhotoResponseEvent
819
845
  photo_status: PhotoStatusEvent
820
846
  video_recording_status: VideoRecordingStatusEvent
847
+ media_success: MediaUploadSuccessEvent
848
+ media_error: MediaUploadErrorEvent
821
849
  gallery_status: GalleryStatusEvent
822
850
  compatible_glasses_search_stop: CompatibleGlassesSearchStopEvent
823
851
  swipe_volume_status: SwipeVolumeStatusEvent
@@ -898,7 +926,18 @@ export interface BluetoothSdkPublicModule {
898
926
  sound: boolean,
899
927
  settings?: VideoRecordingSettings,
900
928
  ): Promise<VideoRecordingStartedStatusEvent>
901
- stopVideoRecording(requestId: string): Promise<VideoRecordingStoppedStatusEvent>
929
+ /**
930
+ * Stop the active recording. When {@link webhookUrl} is provided, the glasses
931
+ * upload the recorded video to it (multipart) using {@link authToken}. These
932
+ * are supplied at stop time (not start) so the token is fresh when the upload
933
+ * runs — a recording can last arbitrarily long. An empty/omitted webhook keeps
934
+ * the video on device (no upload).
935
+ */
936
+ stopVideoRecording(
937
+ requestId: string,
938
+ webhookUrl?: string,
939
+ authToken?: string,
940
+ ): Promise<VideoRecordingStoppedStatusEvent>
902
941
 
903
942
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
904
943
  stopStream(): Promise<StreamStatusEvent>
@@ -148,7 +148,11 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
148
148
  sound: boolean,
149
149
  settings?: VideoRecordingSettings,
150
150
  ): Promise<VideoRecordingStartedStatusEvent>
151
- stopVideoRecording(requestId: string): Promise<VideoRecordingStoppedStatusEvent>
151
+ stopVideoRecording(
152
+ requestId: string,
153
+ webhookUrl?: string,
154
+ authToken?: string,
155
+ ): Promise<VideoRecordingStoppedStatusEvent>
152
156
 
153
157
  // Stream Commands
154
158
  startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
package/src/index.ts CHANGED
@@ -21,6 +21,8 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
21
21
  "photo_response",
22
22
  "photo_status",
23
23
  "video_recording_status",
24
+ "media_success",
25
+ "media_error",
24
26
  "gallery_status",
25
27
  "compatible_glasses_search_stop",
26
28
  "swipe_volume_status",