@mentra/bluetooth-sdk 0.1.14 → 0.1.16

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 +4 -6
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +22 -9
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +358 -261
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -3
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +133 -52
  7. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
  8. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +22 -6
  9. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +50 -3
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -795
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +422 -78
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +3 -4
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -1
  16. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
  17. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  18. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
  19. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  20. package/build/BluetoothSdk.types.d.ts +39 -14
  21. package/build/BluetoothSdk.types.d.ts.map +1 -1
  22. package/build/BluetoothSdk.types.js +1 -0
  23. package/build/BluetoothSdk.types.js.map +1 -1
  24. package/build/_internal.d.ts +4 -4
  25. package/build/_internal.js +4 -4
  26. package/build/_internal.js.map +1 -1
  27. package/build/_private/BluetoothSdkModule.d.ts +11 -3
  28. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  29. package/build/_private/BluetoothSdkModule.js +5 -0
  30. package/build/_private/BluetoothSdkModule.js.map +1 -1
  31. package/build/_private/cameraRequestPayload.d.ts +4 -0
  32. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  33. package/build/_private/cameraRequestPayload.js +25 -0
  34. package/build/_private/cameraRequestPayload.js.map +1 -0
  35. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  36. package/build/_private/photoRequestPayload.js +8 -2
  37. package/build/_private/photoRequestPayload.js.map +1 -1
  38. package/build/index.d.ts +1 -1
  39. package/build/index.d.ts.map +1 -1
  40. package/build/index.js +72 -54
  41. package/build/index.js.map +1 -1
  42. package/ios/BluetoothSdkModule.swift +40 -12
  43. package/ios/Source/Bridge.swift +4 -16
  44. package/ios/Source/DeviceManager.swift +72 -12
  45. package/ios/Source/MentraBluetoothSDK.swift +120 -50
  46. package/ios/Source/OtaManifest.swift +13 -13
  47. package/ios/Source/camera/CameraModels.swift +75 -9
  48. package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
  49. package/ios/Source/events/BluetoothEvents.swift +5 -27
  50. package/ios/Source/internal/BluetoothAvailability.swift +5 -5
  51. package/ios/Source/sgcs/G1.swift +51 -1
  52. package/ios/Source/sgcs/G2.swift +643 -203
  53. package/ios/Source/sgcs/MentraLive.swift +53 -39
  54. package/ios/Source/sgcs/Nimo.swift +1928 -0
  55. package/ios/Source/sgcs/SGCManager.swift +12 -5
  56. package/ios/Source/stt/STTTools.swift +1 -1
  57. package/ios/Source/types/DeviceModels.swift +5 -0
  58. package/ios/Source/utils/Constants.swift +2 -0
  59. package/package.json +6 -1
  60. package/src/BluetoothSdk.types.ts +42 -17
  61. package/src/_internal.ts +4 -4
  62. package/src/_private/BluetoothSdkModule.ts +18 -2
  63. package/src/_private/cameraRequestPayload.ts +29 -0
  64. package/src/_private/photoRequestPayload.ts +9 -2
  65. package/src/index.ts +76 -58
@@ -31,6 +31,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
31
31
  "hotspot_error",
32
32
  "photo_response",
33
33
  "photo_status",
34
+ "camera_status",
34
35
  "video_recording_status",
35
36
  "media_success",
36
37
  "media_error",
@@ -58,7 +59,6 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
58
59
  "stream_status",
59
60
  "keep_alive_ack",
60
61
  "mtk_update_complete",
61
- "ota_update_available",
62
62
  "ota_progress",
63
63
  "ota_start_ack",
64
64
  "ota_status",
@@ -154,7 +154,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
154
154
  AsyncFunction("connectWithOptions") { (device: [String: Any], options: [String: Any]) in
155
155
  try await MainActor.run {
156
156
  guard let target = Device(dictionary: device) else {
157
- throw BluetoothError(
157
+ throw BluetoothSdkError(
158
158
  code: "invalid_device",
159
159
  message: "connect requires a Device with model and name."
160
160
  )
@@ -303,7 +303,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
303
303
  timestampMs >= Double(Int64.min),
304
304
  timestampMs <= maxTimestamp
305
305
  else {
306
- throw BluetoothError(
306
+ throw BluetoothSdkError(
307
307
  code: "invalid_timestamp",
308
308
  message: "setSystemTime timestampMs must be a finite Int64 millisecond timestamp."
309
309
  )
@@ -350,6 +350,11 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
350
350
  return try await sdk.setCameraFov(CameraFov(fov: value, roiPosition: roiPosition)).values
351
351
  }
352
352
 
353
+ AsyncFunction("setCameraTuningConfig") { (anrOn: Bool, gainOn: Bool) in
354
+ let sdk = await MainActor.run { self.bluetoothSdk() }
355
+ return try await sdk.setCameraTuningConfig(anrOn: anrOn, gainOn: gainOn).values
356
+ }
357
+
353
358
  AsyncFunction("queryGalleryStatus") {
354
359
  let sdk = await MainActor.run { self.bluetoothSdk() }
355
360
  return try await sdk.queryGalleryStatus().values
@@ -358,13 +363,41 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
358
363
  AsyncFunction("requestPhoto") { (params: [String: Any]) in
359
364
  let req = PhotoRequest.from(params: params)
360
365
  Bridge.log(
361
- "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=\(req.requestId) appId=\(req.appId) size=\(req.size.rawValue) compress=\(req.compress?.rawValue ?? "none") aeDivisor=\(req.aeExposureDivisor.map { String($0) } ?? "nil")"
366
+ "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=\(req.requestId) size=\(req.size.rawValue) compress=\(req.compress?.rawValue ?? "none") aeDivisor=\(req.aeExposureDivisor.map { String($0) } ?? "nil")"
362
367
  )
363
368
 
364
369
  let sdk = await MainActor.run { self.bluetoothSdk() }
365
370
  return try await sdk.requestPhoto(req).values
366
371
  }
367
372
 
373
+ AsyncFunction("warmUpCamera") { (params: [String: Any]) in
374
+ let requestId = params["requestId"] as? String
375
+ let sizeRaw = params["size"] as? String ?? "medium"
376
+ let size = PhotoSize(normalizedRawValue: sizeRaw)
377
+ let exposureTimeNs: Double?
378
+ switch params["exposureTimeNs"] {
379
+ case let value as Double:
380
+ exposureTimeNs = value.isFinite && value > 0 ? value : nil
381
+ case let value as Int:
382
+ exposureTimeNs = value > 0 ? Double(value) : nil
383
+ case let value as NSNumber:
384
+ let d = value.doubleValue
385
+ exposureTimeNs = d.isFinite && d > 0 ? d : nil
386
+ default:
387
+ exposureTimeNs = nil
388
+ }
389
+ let durationRaw = intValue(params["durationMs"]) ?? 0
390
+ let durationMs = durationRaw > 0 ? durationRaw : 15000
391
+
392
+ let sdk = await MainActor.run { self.bluetoothSdk() }
393
+ return try await sdk.warmUpCamera(
394
+ requestId: requestId,
395
+ size: size,
396
+ exposureTimeNs: exposureTimeNs,
397
+ durationMs: durationMs
398
+ ).values
399
+ }
400
+
368
401
  // MARK: - OTA Commands
369
402
 
370
403
  Function("setOtaVersionUrl") { (otaVersionUrl: String) in
@@ -399,11 +432,6 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
399
432
  return try await sdk.sendOtaQueryStatus().values
400
433
  }
401
434
 
402
- AsyncFunction("retryOtaVersionCheck") {
403
- let sdk = await MainActor.run { self.bluetoothSdk() }
404
- return try await sdk.retryOtaVersionCheck().values
405
- }
406
-
407
435
  // MARK: - Version Info Commands
408
436
 
409
437
  AsyncFunction("requestVersionInfo") {
@@ -689,6 +717,8 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
689
717
  sendEvent("photo_response", response.values)
690
718
  case let .photoStatus(status):
691
719
  sendEvent("photo_status", status.values)
720
+ case let .cameraStatus(status):
721
+ sendEvent("camera_status", status.values)
692
722
  case let .videoRecordingStatus(status):
693
723
  sendEvent("video_recording_status", status.values)
694
724
  case let .mediaUpload(event):
@@ -699,8 +729,6 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
699
729
  sendEvent("stream_status", status.values)
700
730
  case let .keepAliveAck(ack):
701
731
  sendEvent("keep_alive_ack", ack.values)
702
- case let .otaUpdateAvailable(event):
703
- sendEvent("ota_update_available", event.values)
704
732
  case let .otaStartAck(event):
705
733
  sendEvent("ota_start_ack", event.values)
706
734
  case let .otaStatus(event):
@@ -743,7 +771,7 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
743
771
  }
744
772
 
745
773
  @MainActor
746
- public func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail error: BluetoothError) {
774
+ public func mentraBluetoothSDK(_: MentraBluetoothSDK, didFail error: BluetoothSdkError) {
747
775
  sendEvent("pair_failure", ["error": error.message])
748
776
  }
749
777
  }
@@ -347,6 +347,10 @@ class Bridge {
347
347
  Bridge.sendTypedMessage("photo_status", body: status)
348
348
  }
349
349
 
350
+ static func sendCameraStatus(_ status: [String: Any]) {
351
+ Bridge.sendTypedMessage("camera_status", body: status)
352
+ }
353
+
350
354
  static func sendPhotoResponse(_ response: [String: Any]) {
351
355
  Bridge.sendTypedMessage("photo_response", body: response)
352
356
  }
@@ -434,22 +438,6 @@ class Bridge {
434
438
  Bridge.sendTypedMessage("ota_start_ack", body: eventBody)
435
439
  }
436
440
 
437
- /// Send OTA update available notification - glasses have detected an available update (background mode)
438
- static func sendOtaUpdateAvailable(
439
- versionCode: Int64,
440
- versionName: String,
441
- updates: [String],
442
- totalSize: Int64
443
- ) {
444
- let eventBody: [String: Any] = [
445
- "version_code": versionCode,
446
- "version_name": versionName,
447
- "updates": updates,
448
- "total_size": totalSize,
449
- ]
450
- Bridge.sendTypedMessage("ota_update_available", body: eventBody)
451
- }
452
-
453
441
  static func sendOtaStatus(
454
442
  sessionId: String,
455
443
  totalSteps: Int,
@@ -22,11 +22,14 @@ struct ViewState {
22
22
  var text: String
23
23
  var data: String?
24
24
  var animationData: [String: Any]?
25
- // Optional bitmap_view container position/size (used by G2; ignored by others)
26
- var bmpX: Int32?
27
- var bmpY: Int32?
28
- var bmpWidth: Int32?
29
- var bmpHeight: Int32?
25
+ // Optional container position/size used by bitmap_view and positioned_text (G2; ignored by others)
26
+ var bmpX: Int32? = nil
27
+ var bmpY: Int32? = nil
28
+ var bmpWidth: Int32? = nil
29
+ var bmpHeight: Int32? = nil
30
+ // Optional positioned_text border (used by G2; ignored by others)
31
+ var borderWidth: Int32? = nil
32
+ var borderRadius: Int32? = nil
30
33
  }
31
34
 
32
35
  @MainActor
@@ -620,6 +623,8 @@ struct ViewState {
620
623
  sgc = G2()
621
624
  } else if wearable.contains(DeviceTypes.LIVE) {
622
625
  sgc = MentraLive()
626
+ } else if wearable.contains(DeviceTypes.NIMO) {
627
+ sgc = Nimo()
623
628
  } else if wearable.contains(DeviceTypes.FRAME) {
624
629
  // sgc = FrameManager()
625
630
  }
@@ -714,6 +719,19 @@ struct ViewState {
714
719
  width: currentViewState.bmpWidth,
715
720
  height: currentViewState.bmpHeight
716
721
  )
722
+ case "positioned_text":
723
+ Bridge.log(
724
+ "MAN: positioned_text → text='\(currentViewState.text)' rect=\(currentViewState.bmpX ?? 0),\(currentViewState.bmpY ?? 0) \(currentViewState.bmpWidth ?? 576)x\(currentViewState.bmpHeight ?? 288)"
725
+ )
726
+ await sgc?.sendPositionedText(
727
+ currentViewState.text,
728
+ x: currentViewState.bmpX ?? 0,
729
+ y: currentViewState.bmpY ?? 0,
730
+ width: currentViewState.bmpWidth ?? 576,
731
+ height: currentViewState.bmpHeight ?? 288,
732
+ borderWidth: currentViewState.borderWidth ?? 0,
733
+ borderRadius: currentViewState.borderRadius ?? 0
734
+ )
717
735
  case "clear_view":
718
736
  sgc?.clearDisplay()
719
737
  default:
@@ -1039,6 +1057,8 @@ struct ViewState {
1039
1057
  let bmpY = (layout["y"] as? NSNumber).map { $0.int32Value }
1040
1058
  let bmpWidth = (layout["width"] as? NSNumber).map { $0.int32Value }
1041
1059
  let bmpHeight = (layout["height"] as? NSNumber).map { $0.int32Value }
1060
+ let borderWidth = (layout["borderWidth"] as? NSNumber).map { $0.int32Value }
1061
+ let borderRadius = (layout["borderRadius"] as? NSNumber).map { $0.int32Value }
1042
1062
 
1043
1063
  text = parsePlaceholders(text)
1044
1064
  topText = parsePlaceholders(topText)
@@ -1048,7 +1068,8 @@ struct ViewState {
1048
1068
  var newViewState = ViewState(
1049
1069
  topText: topText, bottomText: bottomText, title: title, layoutType: layoutType,
1050
1070
  text: text, data: data, animationData: nil,
1051
- bmpX: bmpX, bmpY: bmpY, bmpWidth: bmpWidth, bmpHeight: bmpHeight
1071
+ bmpX: bmpX, bmpY: bmpY, bmpWidth: bmpWidth, bmpHeight: bmpHeight,
1072
+ borderWidth: borderWidth, borderRadius: borderRadius
1052
1073
  )
1053
1074
 
1054
1075
  if layoutType == "bitmap_animation" {
@@ -1069,6 +1090,27 @@ struct ViewState {
1069
1090
  }
1070
1091
  }
1071
1092
 
1093
+ // positioned_text is a sticky overlay container (e.g. the nav trip-stats
1094
+ // label) that lives ALONGSIDE the main view. It must NOT flow through the
1095
+ // single replaceable viewState[stateIndex] — otherwise the constantly-
1096
+ // refreshing main text_wall (maneuver text) and the minimap bitmap_view
1097
+ // overwrite it every frame and it never renders. Route it straight to the
1098
+ // SGC, which keeps its own persistent text container for that rect.
1099
+ if layoutType == "positioned_text" {
1100
+ Bridge.log(
1101
+ "MAN: positioned_text (sticky) → text='\(text)' rect=\(bmpX ?? 0),\(bmpY ?? 0) \(bmpWidth ?? 576)x\(bmpHeight ?? 288)"
1102
+ )
1103
+ Task { [weak self] in
1104
+ await self?.sgc?.sendPositionedText(
1105
+ text,
1106
+ x: bmpX ?? 0, y: bmpY ?? 0,
1107
+ width: bmpWidth ?? 576, height: bmpHeight ?? 288,
1108
+ borderWidth: borderWidth ?? 0, borderRadius: borderRadius ?? 0
1109
+ )
1110
+ }
1111
+ return
1112
+ }
1113
+
1072
1114
  let cS = viewStates[stateIndex]
1073
1115
  let nS = newViewState
1074
1116
  let currentState =
@@ -1180,7 +1222,7 @@ struct ViewState {
1180
1222
 
1181
1223
  private func liveSgc() throws -> MentraLive {
1182
1224
  guard let live = sgc as? MentraLive else {
1183
- throw BluetoothError(code: "unsupported_device", message: "This command requires Mentra Live glasses.")
1225
+ throw BluetoothSdkError(code: "unsupported_device", message: "This command requires Mentra Live glasses.")
1184
1226
  }
1185
1227
  return live
1186
1228
  }
@@ -1212,9 +1254,28 @@ struct ViewState {
1212
1254
  try liveSgc().sendCameraFovSetting(requestId: requestId, fov: fov, roiPosition: roiPosition)
1213
1255
  }
1214
1256
 
1215
- func retryOtaVersionCheck() {
1216
- Bridge.log("MAN: Retrying glasses OTA version check after clock sync")
1217
- (sgc as? MentraLive)?.sendOtaRetryVersionCheck()
1257
+ func sendCameraTuningConfig(requestId: String, anrOn: Bool, gainOn: Bool) throws {
1258
+ try liveSgc().sendCameraTuningConfig(requestId: requestId, anrOn: anrOn, gainOn: gainOn)
1259
+ }
1260
+
1261
+ func warmUpCamera(
1262
+ requestId: String,
1263
+ size: PhotoSize,
1264
+ exposureTimeNs: Double?,
1265
+ durationMs: Int
1266
+ ) throws {
1267
+ guard let live = sgc as? MentraLive else {
1268
+ // Fail fast like other camera commands so the SDK promise rejects immediately instead
1269
+ // of hanging until the request timeout with no camera_status.
1270
+ throw BluetoothSdkError(
1271
+ code: "unsupported_device", message: "This command requires Mentra Live glasses.")
1272
+ }
1273
+ live.warmUpCamera(
1274
+ requestId: requestId,
1275
+ size: size,
1276
+ exposureTimeNs: exposureTimeNs,
1277
+ durationMs: durationMs
1278
+ )
1218
1279
  }
1219
1280
 
1220
1281
  /// Request version info from glasses.
@@ -1324,7 +1385,6 @@ struct ViewState {
1324
1385
  let manualIso = manualExposureNs != nil ? request.iso.flatMap { $0 > 0 ? $0 : nil } : nil
1325
1386
  let routed = PhotoRequest(
1326
1387
  requestId: request.requestId,
1327
- appId: request.appId,
1328
1388
  size: request.size,
1329
1389
  webhookUrl: request.webhookUrl,
1330
1390
  authToken: request.authToken,
@@ -1343,7 +1403,7 @@ struct ViewState {
1343
1403
  ispAnalogGain: request.ispAnalogGain
1344
1404
  )
1345
1405
  Bridge.log(
1346
- "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=\(routed.requestId) appId=\(routed.appId) webhookUrl=\(routed.webhookUrl ?? "nil") size=\(routed.size.rawValue) compress=\(routed.compress?.rawValue ?? "none") save=\(routed.save) sound=\(routed.sound) exposureTimeNs=\(manualExposureNs.map { String($0) } ?? "nil") iso=\(manualIso.map { String($0) } ?? "auto") aeDivisor=\(routed.aeExposureDivisor.map { String($0) } ?? "nil") isoCap=\(routed.isoCap.map { String($0) } ?? "nil") sgc=\(sgc != nil ? String(describing: type(of: sgc!)) : "null")"
1406
+ "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=\(routed.requestId) webhookUrl=\(routed.webhookUrl ?? "nil") size=\(routed.size.rawValue) compress=\(routed.compress?.rawValue ?? "none") save=\(routed.save) sound=\(routed.sound) exposureTimeNs=\(manualExposureNs.map { String($0) } ?? "nil") iso=\(manualIso.map { String($0) } ?? "auto") aeDivisor=\(routed.aeExposureDivisor.map { String($0) } ?? "nil") isoCap=\(routed.isoCap.map { String($0) } ?? "nil") sgc=\(sgc != nil ? String(describing: type(of: sgc!)) : "null")"
1347
1407
  )
1348
1408
  guard let sgc else {
1349
1409
  Bridge.log(