@mentra/bluetooth-sdk 3.2.0-dev.235 → 3.2.0-dev.245

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 (40) hide show
  1. package/README.md +48 -3
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +2 -23
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +44 -19
  7. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +33 -8
  8. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +14 -14
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +31 -6
  10. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +32 -0
  11. package/build/BluetoothSdk.types.d.ts +14 -6
  12. package/build/BluetoothSdk.types.d.ts.map +1 -1
  13. package/build/BluetoothSdk.types.js.map +1 -1
  14. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  15. package/build/_private/photoRequestPayload.js +5 -1
  16. package/build/_private/photoRequestPayload.js.map +1 -1
  17. package/build/generated/changelogCatalog.d.ts +3 -0
  18. package/build/generated/changelogCatalog.d.ts.map +1 -1
  19. package/build/generated/changelogCatalog.js +4 -0
  20. package/build/generated/changelogCatalog.js.map +1 -1
  21. package/build/generated/releaseMetadata.js +5 -5
  22. package/build/generated/releaseMetadata.js.map +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +6 -1
  25. package/build/index.js.map +1 -1
  26. package/ios/BluetoothSdkModule.swift +1 -1
  27. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  28. package/ios/Source/DeviceManager.swift +1 -1
  29. package/ios/Source/GeneratedChangelogCatalog.swift +1 -0
  30. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  31. package/ios/Source/MentraBluetoothSDK.swift +52 -25
  32. package/ios/Source/camera/CameraModels.swift +29 -15
  33. package/ios/Source/sgcs/MentraLive.swift +35 -4
  34. package/ios/Tests/PhotoCompressionTests.swift +21 -0
  35. package/package.json +4 -2
  36. package/src/BluetoothSdk.types.ts +15 -4
  37. package/src/_private/photoRequestPayload.ts +5 -1
  38. package/src/generated/changelogCatalog.ts +4 -0
  39. package/src/generated/releaseMetadata.ts +5 -5
  40. package/src/index.ts +6 -1
@@ -810,6 +810,7 @@ private struct FileTransferSession {
810
810
  }
811
811
 
812
812
  private struct BlePhotoTransfer {
813
+ var isThumbnail = false
813
814
  let bleImgId: String
814
815
  let requestId: String
815
816
  let webhookUrl: String
@@ -2059,6 +2060,7 @@ class MentraLive: NSObject, SGCManager {
2059
2060
  "I" + String(format: "%09d", Int(Date().timeIntervalSince1970 * 1000) % 100_000_000)
2060
2061
  json["bleImgId"] = bleImgId
2061
2062
  json["transferMethod"] = request.transferMethod
2063
+ if request.presendThumbnail { json["presend_thumbnail"] = true }
2062
2064
 
2063
2065
  if let webhookUrl = request.webhookUrl, !webhookUrl.isEmpty {
2064
2066
  json["webhookUrl"] = webhookUrl
@@ -2085,7 +2087,7 @@ class MentraLive: NSObject, SGCManager {
2085
2087
  json["size"] = allowedSizes.contains(size) ? size : "medium"
2086
2088
  json["mode"] = request.mode.rawValue
2087
2089
 
2088
- json["compress"] = request.compress?.rawValue ?? "none"
2090
+ json["compress"] = request.compress.rawValue
2089
2091
  json["save"] = request.save
2090
2092
  json["sound"] = request.sound
2091
2093
 
@@ -4003,6 +4005,20 @@ class MentraLive: NSObject, SGCManager {
4003
4005
  let bleImgId = json["bleImgId"] as? String ?? ""
4004
4006
  let requestId = json["requestId"] as? String ?? ""
4005
4007
  let compressionDurationMs = json["compressionDurationMs"] as? Int64 ?? 0
4008
+ if json["thumbnail"] as? Bool == true, !bleImgId.isEmpty, !requestId.isEmpty {
4009
+ guard blePhotoTransfers.values.contains(where: {
4010
+ !$0.isThumbnail && $0.requestId == requestId && bleImgId == "T" + $0.bleImgId.dropFirst()
4011
+ }) else {
4012
+ Bridge.log("LIVE: Ignoring thumbnail for an unknown photo request")
4013
+ return
4014
+ }
4015
+ // Preserve any packets already received when the ready message is repeated.
4016
+ if blePhotoTransfers[bleImgId] == nil {
4017
+ var transfer = BlePhotoTransfer(bleImgId: bleImgId, requestId: requestId, webhookUrl: "")
4018
+ transfer.isThumbnail = true
4019
+ blePhotoTransfers[bleImgId] = transfer
4020
+ }
4021
+ }
4006
4022
 
4007
4023
  Bridge.log(
4008
4024
  "LIVE: 📸 BLE photo ready notification: bleImgId=\(bleImgId), requestId=\(requestId)"
@@ -4480,6 +4496,17 @@ class MentraLive: NSObject, SGCManager {
4480
4496
  }
4481
4497
 
4482
4498
  private func processAndUploadBlePhoto(_ transfer: BlePhotoTransfer, imageData: Data) {
4499
+ if transfer.isThumbnail {
4500
+ Bridge.sendPhotoStatus([
4501
+ "type": "photo_status",
4502
+ "requestId": transfer.requestId,
4503
+ "status": "thumbnail_received",
4504
+ "timestamp": Int64(Date().timeIntervalSince1970 * 1000),
4505
+ "thumbnailUrl": "data:image/jpeg;base64," + imageData.base64EncodedString(),
4506
+ "fileSizeBytes": imageData.count,
4507
+ ])
4508
+ return
4509
+ }
4483
4510
  Bridge.log("LIVE: Processing BLE photo for upload. RequestId: \(transfer.requestId)")
4484
4511
 
4485
4512
  BlePhotoUploadService.processAndUploadPhoto(
@@ -6888,6 +6915,10 @@ extension MentraLive {
6888
6915
  let aeExposureDivisor = DeviceStore.shared.get("bluetooth", "button_photo_ae_exposure_divisor") as? Int
6889
6916
  let isoCap = DeviceStore.shared.get("bluetooth", "button_photo_iso_cap") as? Int
6890
6917
  let compressStr = DeviceStore.shared.get("bluetooth", "button_photo_compress") as? String
6918
+ if let compressStr, PhotoCompression(rawValue: compressStr) == nil {
6919
+ Bridge.log("LIVE: Invalid stored photo compression: \(compressStr)")
6920
+ return
6921
+ }
6891
6922
  let sound = DeviceStore.shared.get("bluetooth", "button_photo_sound") as? Bool
6892
6923
 
6893
6924
  let settings = PhotoCaptureDefaults(
@@ -6900,7 +6931,7 @@ extension MentraLive {
6900
6931
  ispAnalogGain: ispAnalogGain,
6901
6932
  aeExposureDivisor: aeExposureDivisor,
6902
6933
  isoCap: isoCap,
6903
- compress: compressStr,
6934
+ compress: compressStr.flatMap(PhotoCompression.init(rawValue:)),
6904
6935
  sound: sound,
6905
6936
  resetCaptureTuning: false
6906
6937
  )
@@ -6984,8 +7015,8 @@ extension MentraLive {
6984
7015
  if let isoCap = settings.isoCap, isoCap > 0 {
6985
7016
  json["isoCap"] = isoCap
6986
7017
  }
6987
- if let compress = settings.compress, !compress.isEmpty {
6988
- json["compress"] = compress
7018
+ if let compress = settings.compress {
7019
+ json["compress"] = compress.rawValue
6989
7020
  }
6990
7021
  if let sound = settings.sound {
6991
7022
  json["sound"] = sound
@@ -0,0 +1,21 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ final class PhotoCompressionTests: XCTestCase {
5
+ func testCompressionIsPreservedAndDefaultsToNone() throws {
6
+ for raw in ["none", "low", "medium", "high"] {
7
+ XCTAssertEqual(try PhotoRequest.from(params: ["compress": raw]).compress.rawValue, raw)
8
+ XCTAssertEqual(try PhotoCaptureDefaults.from(params: ["compress": raw]).compress?.rawValue, raw)
9
+ }
10
+ XCTAssertEqual(PhotoRequest(size: .medium, sound: true).compress, .none)
11
+ XCTAssertEqual(try PhotoRequest.from(params: [:]).compress, .none)
12
+ XCTAssertNil(try PhotoCaptureDefaults.from(params: [:]).compress)
13
+ }
14
+
15
+ func testInvalidCompressionIsRejected() {
16
+ for invalid: Any in ["heavy", "", "HIGH", NSNull(), 1, false] {
17
+ XCTAssertThrowsError(try PhotoRequest.from(params: ["compress": invalid]))
18
+ XCTAssertThrowsError(try PhotoCaptureDefaults.from(params: ["compress": invalid]))
19
+ }
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.2.0-dev.235",
3
+ "version": "3.2.0-dev.245",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -113,7 +113,9 @@
113
113
  "access": "public",
114
114
  "registry": "https://registry.npmjs.org/"
115
115
  },
116
- "dependencies": {},
116
+ "dependencies": {
117
+ "@mentra/cloud-protocol": "3.2.0-dev.245"
118
+ },
117
119
  "devDependencies": {
118
120
  "@types/node": "^25.9.3",
119
121
  "expo-module-scripts": "^55.0.2",
@@ -1,3 +1,6 @@
1
+ import type {PhotoCompression} from "@mentra/cloud-protocol/photo-compression"
2
+ export type {PhotoCompression} from "@mentra/cloud-protocol/photo-compression"
3
+
1
4
  // Bluetooth SDK Event Types
2
5
  export type GlassesNotReadyEvent = {
3
6
  type: "glasses_not_ready"
@@ -284,6 +287,7 @@ export type PhotoStatusState =
284
287
  | "uploaded"
285
288
  | "ready_for_transfer"
286
289
  | "transferring"
290
+ | "thumbnail_received"
287
291
  | "failed"
288
292
 
289
293
  export type PhotoResolvedConfig = {
@@ -294,7 +298,7 @@ export type PhotoResolvedConfig = {
294
298
  requestedSize?: PhotoSize | string
295
299
  source?: "sdk" | "button" | string
296
300
  transferMethod?: "webhook" | "ble" | "local" | string
297
- compression?: PhotoCompression | string
301
+ compression?: PhotoCompression
298
302
  saveToGallery?: boolean
299
303
  exposureTimeNs?: number
300
304
  iso?: number
@@ -360,6 +364,9 @@ export type PhotoStatusEvent = {
360
364
  captureMetadata?: PhotoCaptureMetadata
361
365
  errorCode?: string
362
366
  errorMessage?: string
367
+ /** Present on thumbnail_received: a JPEG data URI, not the final photo. */
368
+ thumbnailUrl?: string
369
+ fileSizeBytes?: number
363
370
  }
364
371
 
365
372
  export type CameraStatusEvent = {
@@ -561,7 +568,6 @@ export type PhotoCaptureDefaults = {
561
568
  /** When true, clears stored NR/edge/ISP presets on the glasses before applying other fields. */
562
569
  resetCaptureTuning?: boolean
563
570
  }
564
- export type PhotoCompression = "none" | "medium" | "heavy"
565
571
 
566
572
  export type VideoRecordingDefaults = {
567
573
  width: number
@@ -653,6 +659,11 @@ export type MicPreference = "auto" | "phone" | "glasses" | "bluetooth"
653
659
  export type MicMode = "phone" | "glasses" | "bluetoothClassic" | "bluetooth"
654
660
 
655
661
  export type PhotoRequestParams = {
662
+ /**
663
+ * Send an oriented <=500px, quality-50 JPEG preview before the full photo. Default false.
664
+ * Enables a 110-second end-to-end native deadline (ordinary requests: 30 seconds).
665
+ */
666
+ presend_thumbnail?: boolean
656
667
  requestId?: string
657
668
  appId?: string
658
669
  size: PhotoSize
@@ -661,7 +672,7 @@ export type PhotoRequestParams = {
661
672
  transferMethod?: PhotoTransferMethod
662
673
  webhookUrl: string | null
663
674
  authToken: string | null
664
- compress: PhotoCompression
675
+ compress?: PhotoCompression
665
676
  save?: boolean
666
677
  sound: boolean
667
678
  exposureTimeNs?: number | null
@@ -688,7 +699,7 @@ export type WarmUpCameraParams = {
688
699
  size: PhotoSize
689
700
  mode?: PhotoMode
690
701
  exposureTimeNs?: number | null
691
- /** Ready-state hold; defaults to 15 seconds and is capped at 60 seconds by ASG. */
702
+ /** Ready-state hold; defaults to 15 seconds and is capped at 5 minutes by ASG. */
692
703
  durationMs?: number
693
704
  /** ZSL preview buffering for the warm-up session. */
694
705
  zsl?: boolean
@@ -1,3 +1,4 @@
1
+ import {parsePhotoCompression} from "@mentra/cloud-protocol/photo-compression"
1
2
  import type {PhotoRequestParams, PhotoSize, PhotoTransferMethod} from "../BluetoothSdk.types"
2
3
 
3
4
  const PHOTO_TRANSFER_METHODS = new Set<PhotoTransferMethod>(["auto", "direct", "ble"])
@@ -41,10 +42,13 @@ export function photoRequestParamsForNative(params: PhotoRequestParams): Record<
41
42
  mode: params.mode ?? "photo",
42
43
  transferMethod: photoTransferMethodForNative(params.transferMethod),
43
44
  webhookUrl: params.webhookUrl ?? "",
44
- compress: params.compress,
45
+ compress: parsePhotoCompression(params.compress),
45
46
  sound: params.sound,
46
47
  }
47
48
  const requestId = nonBlankString(params.requestId)
49
+ if (params.presend_thumbnail === true) {
50
+ payload.presend_thumbnail = true
51
+ }
48
52
  if (requestId != null) {
49
53
  payload.requestId = requestId
50
54
  }
@@ -5,6 +5,10 @@ export const GENERATED_RELEASE_CHANGELOGS = Object.freeze(
5
5
  "version": "3.2.0",
6
6
  "markdown": "This release is under active development. User-facing changes will be documented as they land.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."
7
7
  },
8
+ {
9
+ "version": "3.1.1",
10
+ "markdown": "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- MentraOS, Mentra Engine, the Bluetooth SDK, and the glasses client now share one coordinated release version.\n- Mentra Live updates can continue across APK, system, and firmware restarts without asking the user to start the same update again.\n- Bluetooth photo capture and transfer diagnostics are more precise and less disruptive to normal glasses traffic.\n- Bluetooth SDK usage analytics now report the host app's version, build type, and install source (store, TestFlight, sideload, simulator), plus the glasses firmware versions on identification, so store usage can be measured separately from development builds.\n- Bluetooth SDK usage analytics now identify G2 glasses by serial, keep long-lived connections visible across day boundaries, and retry failed uploads instead of dropping them."
11
+ },
8
12
  {
9
13
  "version": "3.1.0",
10
14
  "markdown": "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- MentraOS, Mentra Engine, the Bluetooth SDK, and the glasses client now share one coordinated release version.\n- Mentra Live updates can continue across APK, system, and firmware restarts without asking the user to start the same update again.\n- Bluetooth photo capture and transfer diagnostics are more precise and less disruptive to normal glasses traffic.\n- Bluetooth SDK usage analytics now report the host app's version, build type, and install source (store, TestFlight, sideload, simulator), plus the glasses firmware versions on identification, so store usage can be measured separately from development builds.\n- Bluetooth SDK usage analytics now identify G2 glasses by serial, keep long-lived connections visible across day boundaries, and retry failed uploads instead of dropping them."
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.0",
15
- "releaseIdentity": "3.2.0-dev.235",
16
- "releaseSetId": "mentra-3.2.0-dev.235",
17
- "sourceCommit": "5f25e2687788b3d964e19d17fd6cd4139e3e677e",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.235.json",
19
- "otaManifestSha256": "a9e9ebec66e2f638ab89f446d87925cdc2c85abf3091ed33e76f01986e658ca7"
15
+ "releaseIdentity": "3.2.0-dev.245",
16
+ "releaseSetId": "mentra-3.2.0-dev.245",
17
+ "sourceCommit": "979b6f01635a92d0061160e9007ccb8be313b06e",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.245.json",
19
+ "otaManifestSha256": "b9e030e64254b2d39e89a4e446032ec2d996702a5f248d71a5bad390f99ef914"
20
20
  })
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import {parsePhotoCompression} from "@mentra/cloud-protocol/photo-compression"
1
2
  import PrivateBluetoothSdkModule from "./_private/BluetoothSdkModule"
2
3
  import type {
3
4
  BluetoothSdkEventListener,
4
5
  BluetoothSdkEventName,
5
6
  BluetoothSdkPublicModule,
7
+ PhotoCaptureDefaults,
6
8
  PublicBluetoothStatus,
7
9
  PublicGlassesStatus,
8
10
  VideoRecordingDefaults,
@@ -133,7 +135,10 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
133
135
  * `requestPhoto(...)` options (e.g. `mode: "text"` for text sensor size/crop, or explicit per-shot
134
136
  * fields). Still functional until removed in a future release.
135
137
  */
136
- setPhotoCaptureDefaults: bindPublicMethod("setPhotoCaptureDefaults"),
138
+ setPhotoCaptureDefaults: (settings: PhotoCaptureDefaults) => {
139
+ if (settings.compress !== undefined) parsePhotoCompression(settings.compress)
140
+ return bindPublicMethod("setPhotoCaptureDefaults")(settings)
141
+ },
137
142
  setVideoRecordingDefaults: ({width, height, fps}: VideoRecordingDefaults) => {
138
143
  const method = (PrivateBluetoothSdkModule as unknown as Record<string, unknown>).setVideoRecordingDefaults
139
144
  if (typeof method !== "function") {