@mentra/bluetooth-sdk 0.1.12 → 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 (65) hide show
  1. package/README.md +16 -9
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +51 -3
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +73 -21
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +190 -26
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +78 -10
  10. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
  11. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
  12. package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -13
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
  20. package/build/BluetoothSdk.types.d.ts +39 -9
  21. package/build/BluetoothSdk.types.d.ts.map +1 -1
  22. package/build/BluetoothSdk.types.js.map +1 -1
  23. package/build/_private/BluetoothSdkModule.d.ts +8 -6
  24. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  25. package/build/_private/BluetoothSdkModule.js +0 -2
  26. package/build/_private/BluetoothSdkModule.js.map +1 -1
  27. package/build/_private/photoRequestPayload.d.ts +3 -1
  28. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  29. package/build/_private/photoRequestPayload.js +43 -1
  30. package/build/_private/photoRequestPayload.js.map +1 -1
  31. package/build/debug.d.ts +3 -0
  32. package/build/debug.d.ts.map +1 -0
  33. package/build/debug.js +8 -0
  34. package/build/debug.js.map +1 -0
  35. package/build/index.d.ts +1 -1
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js +14 -5
  38. package/build/index.js.map +1 -1
  39. package/ios/BluetoothSdkModule.swift +38 -57
  40. package/ios/Source/BluetoothSdkDefaults.swift +23 -3
  41. package/ios/Source/DeviceManager.swift +37 -26
  42. package/ios/Source/DeviceStore.swift +5 -1
  43. package/ios/Source/MentraBluetoothSDK.swift +197 -26
  44. package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
  45. package/ios/Source/ObservableStore.swift +11 -0
  46. package/ios/Source/OtaManifest.swift +170 -0
  47. package/ios/Source/camera/CameraModels.swift +218 -8
  48. package/ios/Source/controllers/ControllerManager.swift +2 -5
  49. package/ios/Source/controllers/R1.swift +2 -5
  50. package/ios/Source/sgcs/G1.swift +6 -5
  51. package/ios/Source/sgcs/G2.swift +20 -18
  52. package/ios/Source/sgcs/Mach1.swift +6 -5
  53. package/ios/Source/sgcs/MentraLive.swift +129 -33
  54. package/ios/Source/sgcs/MentraNex.swift +6 -5
  55. package/ios/Source/sgcs/SGCManager.swift +3 -5
  56. package/ios/Source/sgcs/Simulated.swift +7 -3
  57. package/ios/Source/status/DeviceStatus.swift +1 -1
  58. package/package.json +6 -1
  59. package/src/BluetoothSdk.types.ts +40 -9
  60. package/src/_private/BluetoothSdkModule.ts +8 -9
  61. package/src/_private/photoRequestPayload.ts +45 -2
  62. package/src/debug.ts +9 -0
  63. package/src/index.ts +23 -6
  64. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
  65. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7434
@@ -1234,6 +1234,9 @@ class MentraLive: NSObject, SGCManager {
1234
1234
  func getBatteryStatus() {}
1235
1235
  func setBrightness(_: Int, autoMode _: Bool) {}
1236
1236
  func clearDisplay() {}
1237
+ func sendText(_ text: String) async {
1238
+ await sendTextWall(text)
1239
+ }
1237
1240
  func sendTextWall(_: String) async {}
1238
1241
  func ping() {
1239
1242
  Bridge.log("LIVE: ping()")
@@ -1611,18 +1614,15 @@ class MentraLive: NSObject, SGCManager {
1611
1614
  }
1612
1615
  }
1613
1616
 
1614
- func requestPhoto(
1615
- _ requestId: String, appId: String, size: String?, webhookUrl: String?, authToken: String?,
1616
- compress: String?, flash: Bool, save: Bool, sound: Bool, exposureTimeNs: Double?, iso: Int?
1617
- ) {
1617
+ func requestPhoto(_ request: PhotoRequest) {
1618
1618
  Bridge.log(
1619
- "LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(requestId) appId=\(appId) flash=\(flash) save=\(save) sound=\(sound) iso=\(iso.map { String($0) } ?? "auto")"
1619
+ "LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(request.requestId) appId=\(request.appId) flash=\(request.flash) save=\(request.save) sound=\(request.sound) iso=\(request.iso.map { String($0) } ?? "auto") aeDivisor=\(request.aeExposureDivisor.map { String($0) } ?? "nil")"
1620
1620
  )
1621
1621
 
1622
1622
  var json: [String: Any] = [
1623
1623
  "type": "take_photo",
1624
- "requestId": requestId,
1625
- "appId": appId,
1624
+ "requestId": request.requestId,
1625
+ "appId": request.appId,
1626
1626
  ]
1627
1627
 
1628
1628
  // Always generate BLE ID for potential fallback
@@ -1631,15 +1631,15 @@ class MentraLive: NSObject, SGCManager {
1631
1631
  json["bleImgId"] = bleImgId
1632
1632
  json["transferMethod"] = "auto"
1633
1633
 
1634
- if let webhookUrl, !webhookUrl.isEmpty {
1634
+ if let webhookUrl = request.webhookUrl, !webhookUrl.isEmpty {
1635
1635
  json["webhookUrl"] = webhookUrl
1636
1636
 
1637
1637
  var transfer = BlePhotoTransfer(
1638
- bleImgId: bleImgId, requestId: requestId, webhookUrl: webhookUrl
1638
+ bleImgId: bleImgId, requestId: request.requestId, webhookUrl: webhookUrl
1639
1639
  )
1640
1640
 
1641
1641
  // Store authToken for BLE transfer if provided
1642
- if let authToken, !authToken.isEmpty {
1642
+ if let authToken = request.authToken, !authToken.isEmpty {
1643
1643
  transfer.authToken = authToken
1644
1644
  }
1645
1645
 
@@ -1647,32 +1647,28 @@ class MentraLive: NSObject, SGCManager {
1647
1647
  }
1648
1648
 
1649
1649
  // Add authToken to JSON if provided
1650
- if let authToken, !authToken.isEmpty {
1650
+ if let authToken = request.authToken, !authToken.isEmpty {
1651
1651
  json["authToken"] = authToken
1652
1652
  }
1653
1653
 
1654
- // propagate size (default to medium if invalid)
1655
- if let size, ["small", "medium", "large", "full"].contains(size) {
1656
- json["size"] = size
1657
- } else {
1658
- json["size"] = "medium"
1659
- }
1660
-
1661
- // Add compress parameter
1662
- json["compress"] = compress ?? "none"
1654
+ let allowedSizes = ["low", "medium", "high", "max"]
1655
+ let size = request.size.rawValue
1656
+ json["size"] = allowedSizes.contains(size) ? size : "medium"
1663
1657
 
1664
- json["flash"] = flash
1665
- json["save"] = save
1666
- json["sound"] = sound
1658
+ json["compress"] = request.compress?.rawValue ?? "none"
1659
+ json["flash"] = request.flash
1660
+ json["save"] = request.save
1661
+ json["sound"] = request.sound
1667
1662
 
1668
- if let e = exposureTimeNs, e.isFinite, e > 0, e <= Double(Int64.max) {
1669
- Bridge.log("LIVE: Using manual exposure time for photo request \(requestId): \(Int64(e)) ns")
1663
+ if let e = request.exposureTimeNs, e.isFinite, e > 0, e <= Double(Int64.max) {
1664
+ Bridge.log("LIVE: Using manual exposure time for photo request \(request.requestId): \(Int64(e)) ns")
1670
1665
  json["exposureTimeNs"] = Int64(e)
1671
1666
  }
1672
- if let iso, iso > 0 {
1673
- Bridge.log("LIVE: Using manual ISO for photo request \(requestId): ISO \(iso)")
1667
+ if let iso = request.iso, iso > 0 {
1668
+ Bridge.log("LIVE: Using manual ISO for photo request \(request.requestId): ISO \(iso)")
1674
1669
  json["iso"] = iso
1675
1670
  }
1671
+ request.appendScanFields(to: &json)
1676
1672
 
1677
1673
  Bridge.log("LIVE: PHOTO PIPELINE [5b/6] take_photo JSON ready bleImgId=\(bleImgId) transferMethod=auto")
1678
1674
  Bridge.log("LIVE: PHOTO PIPELINE [6/6] Dispatching take_photo to sendJson()")
@@ -2883,13 +2879,17 @@ class MentraLive: NSObject, SGCManager {
2883
2879
  /// Send OTA start command to glasses.
2884
2880
  /// Called when user approves an update (onboarding or background mode).
2885
2881
  /// Triggers glasses to begin download and installation.
2886
- func sendOtaStart() {
2882
+ func sendOtaStart(otaVersionUrl: String?) {
2887
2883
  Bridge.log("LIVE: 📱 Sending ota_start command to glasses")
2888
2884
 
2889
- let json: [String: Any] = [
2885
+ var json: [String: Any] = [
2890
2886
  "type": "ota_start",
2891
2887
  "timestamp": Int(Date().timeIntervalSince1970 * 1000),
2892
2888
  ]
2889
+ if let otaVersionUrl = otaVersionUrl?.trimmingCharacters(in: .whitespacesAndNewlines),
2890
+ !otaVersionUrl.isEmpty {
2891
+ json["ota_version_url"] = otaVersionUrl
2892
+ }
2893
2893
 
2894
2894
  sendJson(json, wakeUp: true)
2895
2895
  }
@@ -5197,12 +5197,73 @@ extension MentraLive {
5197
5197
  }
5198
5198
 
5199
5199
  func sendButtonPhotoSettings() {
5200
- let size = DeviceStore.shared.get("bluetooth", "button_photo_size") as! String
5201
- sendButtonPhotoSettings(requestId: nil, size: size)
5200
+ let size = DeviceStore.shared.get("bluetooth", "button_photo_size") as? String
5201
+ let mfnr = DeviceStore.shared.get("bluetooth", "button_photo_mfnr") as? Bool
5202
+ let zsl = DeviceStore.shared.get("bluetooth", "button_photo_zsl") as? Bool
5203
+ let noiseReduction = DeviceStore.shared.get("bluetooth", "button_photo_noise_reduction") as? Bool
5204
+ let edgeEnhancement = DeviceStore.shared.get("bluetooth", "button_photo_edge_enhancement") as? Bool
5205
+ let ispDigitalGain = DeviceStore.shared.get("bluetooth", "button_photo_isp_digital_gain") as? Int
5206
+ let ispAnalogGain = DeviceStore.shared.get("bluetooth", "button_photo_isp_analog_gain") as? String
5207
+ let aeExposureDivisor = DeviceStore.shared.get("bluetooth", "button_photo_ae_exposure_divisor") as? Int
5208
+ let isoCap = DeviceStore.shared.get("bluetooth", "button_photo_iso_cap") as? Int
5209
+ let compressStr = DeviceStore.shared.get("bluetooth", "button_photo_compress") as? String
5210
+ let sound = DeviceStore.shared.get("bluetooth", "button_photo_sound") as? Bool
5211
+
5212
+ let settings = ButtonPhotoSettings(
5213
+ size: ButtonPhotoSize(normalizedRawValue: size ?? "medium"),
5214
+ mfnr: mfnr,
5215
+ zsl: zsl,
5216
+ noiseReduction: noiseReduction,
5217
+ edgeEnhancement: edgeEnhancement,
5218
+ ispDigitalGain: ispDigitalGain,
5219
+ ispAnalogGain: ispAnalogGain,
5220
+ aeExposureDivisor: aeExposureDivisor,
5221
+ isoCap: isoCap,
5222
+ compress: compressStr,
5223
+ sound: sound,
5224
+ resetCaptureTuning: false
5225
+ )
5226
+
5227
+ sendButtonPhotoSettings(requestId: nil, settings: settings)
5202
5228
  }
5203
5229
 
5204
5230
  func sendButtonPhotoSettings(requestId: String?, size: String) {
5205
- Bridge.log("Sending button photo setting: \(size)")
5231
+ sendButtonPhotoSettings(requestId: requestId, settings: ButtonPhotoSettings(size: ButtonPhotoSize(normalizedRawValue: size)))
5232
+ }
5233
+
5234
+ func sendButtonPhotoSettings(requestId: String?, settings: ButtonPhotoSettings) {
5235
+ var details = settings.size.map { "size=\($0.rawValue)" } ?? "size=unchanged"
5236
+ if let mfnr = settings.mfnr {
5237
+ details += ", mfnr=\(mfnr)"
5238
+ }
5239
+ if let zsl = settings.zsl {
5240
+ details += ", zsl=\(zsl)"
5241
+ }
5242
+ if let noiseReduction = settings.noiseReduction {
5243
+ details += ", noiseReduction=\(noiseReduction)"
5244
+ }
5245
+ if let edgeEnhancement = settings.edgeEnhancement {
5246
+ details += ", edgeEnhancement=\(edgeEnhancement)"
5247
+ }
5248
+ if let ispDigitalGain = settings.ispDigitalGain {
5249
+ details += ", ispDigitalGain=\(ispDigitalGain)"
5250
+ }
5251
+ if let ispAnalogGain = settings.ispAnalogGain {
5252
+ details += ", ispAnalogGain=\(ispAnalogGain)"
5253
+ }
5254
+ if let aeExposureDivisor = settings.aeExposureDivisor {
5255
+ details += ", aeExposureDivisor=\(aeExposureDivisor)"
5256
+ }
5257
+ if let isoCap = settings.isoCap {
5258
+ details += ", isoCap=\(isoCap)"
5259
+ }
5260
+ if let compress = settings.compress {
5261
+ details += ", compress=\(compress)"
5262
+ }
5263
+ if let sound = settings.sound {
5264
+ details += ", sound=\(sound)"
5265
+ }
5266
+ Bridge.log("Sending button photo setting: \(details)")
5206
5267
 
5207
5268
  guard connectionState == ConnTypes.CONNECTED else {
5208
5269
  Bridge.log("Cannot send button photo settings - not connected")
@@ -5211,11 +5272,46 @@ extension MentraLive {
5211
5272
 
5212
5273
  var json: [String: Any] = [
5213
5274
  "type": "button_photo_setting",
5214
- "size": size,
5215
5275
  ]
5276
+ if let size = settings.size {
5277
+ json["size"] = size.rawValue
5278
+ }
5216
5279
  if let requestId, !requestId.isEmpty {
5217
5280
  json["request_id"] = requestId
5218
5281
  }
5282
+ if let mfnr = settings.mfnr {
5283
+ json["mfnr"] = mfnr
5284
+ }
5285
+ if let zsl = settings.zsl {
5286
+ json["zsl"] = zsl
5287
+ }
5288
+ if let noiseReduction = settings.noiseReduction {
5289
+ json["noiseReduction"] = noiseReduction
5290
+ }
5291
+ if let edgeEnhancement = settings.edgeEnhancement {
5292
+ json["edgeEnhancement"] = edgeEnhancement
5293
+ }
5294
+ if let ispDigitalGain = settings.ispDigitalGain {
5295
+ json["ispDigitalGain"] = ispDigitalGain
5296
+ }
5297
+ if let ispAnalogGain = settings.ispAnalogGain, !ispAnalogGain.isEmpty {
5298
+ json["ispAnalogGain"] = ispAnalogGain
5299
+ }
5300
+ if let aeExposureDivisor = settings.aeExposureDivisor, aeExposureDivisor > 1 {
5301
+ json["aeExposureDivisor"] = aeExposureDivisor
5302
+ }
5303
+ if let isoCap = settings.isoCap, isoCap > 0 {
5304
+ json["isoCap"] = isoCap
5305
+ }
5306
+ if let compress = settings.compress, !compress.isEmpty {
5307
+ json["compress"] = compress
5308
+ }
5309
+ if let sound = settings.sound {
5310
+ json["sound"] = sound
5311
+ }
5312
+ if settings.resetCaptureTuning == true {
5313
+ json["resetCaptureTuning"] = true
5314
+ }
5219
5315
  sendJson(json, wakeUp: true)
5220
5316
  }
5221
5317
 
@@ -51,10 +51,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
51
51
  }
52
52
  }
53
53
 
54
- func requestPhoto(
55
- _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
56
- compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
57
- ) {}
54
+ func requestPhoto(_: PhotoRequest) {}
58
55
 
59
56
  func startStream(_: [String: Any]) {}
60
57
 
@@ -81,6 +78,10 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
81
78
  updateGlassesAutoBrightness(autoMode)
82
79
  }
83
80
 
81
+ func sendText(_ text: String) async {
82
+ await sendTextWall(text)
83
+ }
84
+
84
85
  func sendDoubleTextWall(_ top: String, _ bottom: String) async {
85
86
  await sendTextWall("\(top)\n\(bottom)")
86
87
  }
@@ -178,7 +179,7 @@ class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SG
178
179
 
179
180
  func sendHotspotState(_: Bool) {}
180
181
 
181
- func sendOtaStart() {}
182
+ func sendOtaStart(otaVersionUrl: String?) {}
182
183
  func sendOtaQueryStatus() {}
183
184
 
184
185
  func sendUserEmailToGlasses(_: String) {}
@@ -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])
@@ -54,6 +51,7 @@ protocol SGCManager {
54
51
 
55
52
  func setBrightness(_ level: Int, autoMode: Bool)
56
53
  func clearDisplay()
54
+ func sendText(_ text: String) async
57
55
  func sendTextWall(_ text: String) async
58
56
  func sendDoubleTextWall(_ top: String, _ bottom: String) async
59
57
  /// Display a bitmap. Optional `x`/`y`/`width`/`height` position and size the target
@@ -118,7 +116,7 @@ protocol SGCManager {
118
116
  func sendWifiCredentials(_ ssid: String, _ password: String)
119
117
  func forgetWifiNetwork(_ ssid: String)
120
118
  func sendHotspotState(_ enabled: Bool)
121
- func sendOtaStart()
119
+ func sendOtaStart(otaVersionUrl: String?)
122
120
  func sendOtaQueryStatus()
123
121
  func sendSetSystemTime(_ timestampMs: Int64)
124
122
  func sendOtaRetryVersionCheck()
@@ -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? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.12",
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,
@@ -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 = {
@@ -421,8 +428,24 @@ export type SettingsAckSuccessEvent = Omit<SettingsAckEvent, "status"> & {
421
428
 
422
429
  export type RgbLedAction = "on" | "off"
423
430
  export type RgbLedColor = "red" | "green" | "blue" | "orange" | "white"
424
- export type PhotoSize = "small" | "medium" | "large" | "full"
425
- 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
+ }
426
449
  export type PhotoCompression = "none" | "medium" | "heavy"
427
450
 
428
451
  /**
@@ -511,6 +534,17 @@ export type PhotoRequestParams = {
511
534
  exposureTimeNs?: number | null
512
535
  /** Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure. */
513
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
514
548
  }
515
549
 
516
550
  export type StreamVideoConfig = {
@@ -859,7 +893,6 @@ export type BluetoothSdkEventMap = {
859
893
  mic_pcm: MicPcmEvent
860
894
  mic_lc3: MicLc3Event
861
895
  stream_status: StreamStatusEvent
862
- ota_update_available: OtaUpdateAvailableEvent
863
896
  ota_start_ack: OtaStartAckEvent
864
897
  ota_status: OtaStatusEvent
865
898
  version_info: VersionInfoEvent
@@ -913,7 +946,7 @@ export interface BluetoothSdkPublicModule {
913
946
 
914
947
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>
915
948
  setVoiceActivityDetectionEnabled(enabled: boolean): Promise<void>
916
- setButtonPhotoSettings(size: ButtonPhotoSize): Promise<SettingsAckSuccessEvent>
949
+ setButtonPhotoSettings(settings: ButtonPhotoSettings): Promise<SettingsAckSuccessEvent>
917
950
  setButtonVideoRecordingSettings(width: number, height: number, fps: number): Promise<SettingsAckSuccessEvent>
918
951
  setButtonCameraLed(enabled: boolean): Promise<SettingsAckSuccessEvent>
919
952
  setButtonMaxRecordingTime(minutes: number): Promise<SettingsAckSuccessEvent>
@@ -959,12 +992,10 @@ export interface BluetoothSdkPublicModule {
959
992
  ): Promise<RgbLedControlSuccessResponseEvent>
960
993
 
961
994
  requestVersionInfo(): Promise<VersionInfoResult>
962
- /** Ask connected Mentra Live glasses to check/report OTA availability and status. */
963
- checkForOtaUpdate(): Promise<OtaQueryResult>
964
- /** 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(). */
965
998
  startOtaUpdate(): Promise<OtaStartAckEvent>
966
- /** Re-run the glasses-side OTA version check, mainly after correcting clock skew/TLS failures. */
967
- retryOtaVersionCheck(): Promise<OtaQueryResult>
968
999
 
969
1000
  // // stt commands (MOVE TO CRUST)
970
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>
@@ -571,8 +572,6 @@ const nativeStartStream = NativeBluetoothSdkModule.startStream.bind(NativeBlueto
571
572
  NativeBluetoothSdkModule.startExternallyManagedStream = function (params: StreamStartRequest) {
572
573
  return nativeStartStream({...params, keepAliveMode: "external"} as StreamStartRequest)
573
574
  }
574
- NativeBluetoothSdkModule.checkForOtaUpdate = NativeBluetoothSdkModule.sendOtaQueryStatus.bind(NativeBluetoothSdkModule)
575
- NativeBluetoothSdkModule.startOtaUpdate = NativeBluetoothSdkModule.sendOtaStart.bind(NativeBluetoothSdkModule)
576
575
 
577
576
  export default NativeBluetoothSdkModule
578
- 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",
@@ -36,7 +41,6 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
36
41
  "mic_pcm",
37
42
  "mic_lc3",
38
43
  "stream_status",
39
- "ota_update_available",
40
44
  "ota_start_ack",
41
45
  "ota_status",
42
46
  "version_info",
@@ -50,6 +54,10 @@ const addListener: BluetoothSdkPublicModule["addListener"] = (eventName, listene
50
54
  return PrivateBluetoothSdkModule.addListener(eventName, listener as BluetoothSdkEventListener<BluetoothSdkEventName>)
51
55
  }
52
56
 
57
+ const startOtaUpdate: BluetoothSdkPublicModule["startOtaUpdate"] = () => {
58
+ return PrivateBluetoothSdkModule.startOtaUpdate()
59
+ }
60
+
53
61
  export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
54
62
  addListener,
55
63
  getDefaultDevice: PrivateBluetoothSdkModule.getDefaultDevice.bind(PrivateBluetoothSdkModule),
@@ -77,7 +85,15 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
77
85
  setGalleryModeEnabled: PrivateBluetoothSdkModule.setGalleryModeEnabled.bind(PrivateBluetoothSdkModule),
78
86
  setVoiceActivityDetectionEnabled:
79
87
  PrivateBluetoothSdkModule.setVoiceActivityDetectionEnabled.bind(PrivateBluetoothSdkModule),
80
- 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
+ },
81
97
  setButtonVideoRecordingSettings:
82
98
  PrivateBluetoothSdkModule.setButtonVideoRecordingSettings.bind(PrivateBluetoothSdkModule),
83
99
  setButtonCameraLed: PrivateBluetoothSdkModule.setButtonCameraLed.bind(PrivateBluetoothSdkModule),
@@ -96,9 +112,8 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
96
112
  setGlassesMediaVolume: PrivateBluetoothSdkModule.setGlassesMediaVolume.bind(PrivateBluetoothSdkModule),
97
113
  rgbLedControl: PrivateBluetoothSdkModule.rgbLedControl.bind(PrivateBluetoothSdkModule),
98
114
  requestVersionInfo: PrivateBluetoothSdkModule.requestVersionInfo.bind(PrivateBluetoothSdkModule),
99
- checkForOtaUpdate: PrivateBluetoothSdkModule.sendOtaQueryStatus.bind(PrivateBluetoothSdkModule),
100
- startOtaUpdate: PrivateBluetoothSdkModule.sendOtaStart.bind(PrivateBluetoothSdkModule),
101
- retryOtaVersionCheck: PrivateBluetoothSdkModule.retryOtaVersionCheck.bind(PrivateBluetoothSdkModule),
115
+ checkForOtaUpdate: PrivateBluetoothSdkModule.checkForOtaUpdate.bind(PrivateBluetoothSdkModule),
116
+ startOtaUpdate,
102
117
  setSttModelDetails: PrivateBluetoothSdkModule.setSttModelDetails.bind(PrivateBluetoothSdkModule),
103
118
  getSttModelPath: PrivateBluetoothSdkModule.getSttModelPath.bind(PrivateBluetoothSdkModule),
104
119
  checkSttModelAvailable: PrivateBluetoothSdkModule.checkSttModelAvailable.bind(PrivateBluetoothSdkModule),
@@ -140,6 +155,7 @@ export type {
140
155
  BluetoothSdkPublicModule as BluetoothSdkModule,
141
156
  BluetoothSdkSubscription,
142
157
  ButtonPhotoSize,
158
+ ButtonPhotoSettings,
143
159
  ButtonPressEvent,
144
160
  CameraFovPreset,
145
161
  CameraFovRequest,
@@ -181,6 +197,7 @@ export type {
181
197
  PhotoMeteredPreview,
182
198
  PhotoResponseEvent,
183
199
  PhotoRequestedCaptureConfig,
200
+ PhotoRequestParams,
184
201
  PhotoSize,
185
202
  PhotoStatusEvent,
186
203
  PhotoStatusState,