@mentra/bluetooth-sdk 3.2.0-dev.235 → 3.2.0-dev.247
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.
- package/README.md +48 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +2 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +44 -19
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +33 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +14 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +31 -6
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +32 -0
- package/build/BluetoothSdk.types.d.ts +14 -6
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +5 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +3 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +4 -0
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -1
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +1 -1
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +52 -25
- package/ios/Source/camera/CameraModels.swift +29 -15
- package/ios/Source/sgcs/MentraLive.swift +35 -4
- package/ios/Tests/PhotoCompressionTests.swift +21 -0
- package/package.json +4 -2
- package/src/BluetoothSdk.types.ts +15 -4
- package/src/_private/photoRequestPayload.ts +5 -1
- package/src/generated/changelogCatalog.ts +4 -0
- package/src/generated/releaseMetadata.ts +5 -5
- 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
|
|
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
|
|
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.
|
|
3
|
+
"version": "3.2.0-dev.247",
|
|
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.247"
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
16
|
-
"releaseSetId": "mentra-3.2.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.0-dev.247",
|
|
16
|
+
"releaseSetId": "mentra-3.2.0-dev.247",
|
|
17
|
+
"sourceCommit": "2be1b25e83b0c8db5cef39294c93b0a3f9ec7aef",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.247.json",
|
|
19
|
+
"otaManifestSha256": "cba0e05a180f92e98e3bf47d0acdba2ae81cc79788da1393e80334fb5b26093b"
|
|
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:
|
|
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") {
|