@mentra/bluetooth-sdk 0.1.20 → 0.1.21-beta.0

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 (73) hide show
  1. package/README.md +5 -1
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +127 -24
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +46 -3
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +369 -97
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +3 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +183 -30
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +4 -2
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
  17. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
  18. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +15 -2
  19. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
  20. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  21. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +15 -0
  22. package/build/BluetoothSdk.types.d.ts +46 -1
  23. package/build/BluetoothSdk.types.d.ts.map +1 -1
  24. package/build/BluetoothSdk.types.js.map +1 -1
  25. package/build/_internal.d.ts +2 -0
  26. package/build/_internal.d.ts.map +1 -1
  27. package/build/_internal.js +2 -0
  28. package/build/_internal.js.map +1 -1
  29. package/build/_private/BluetoothSdkModule.d.ts +23 -2
  30. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  31. package/build/_private/BluetoothSdkModule.js +20 -0
  32. package/build/_private/BluetoothSdkModule.js.map +1 -1
  33. package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
  34. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
  35. package/build/_private/MentraLocalNetworkModule.js +3 -0
  36. package/build/_private/MentraLocalNetworkModule.js.map +1 -0
  37. package/build/_private/cameraRequestPayload.d.ts.map +1 -1
  38. package/build/_private/cameraRequestPayload.js +1 -0
  39. package/build/_private/cameraRequestPayload.js.map +1 -1
  40. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  41. package/build/_private/photoRequestPayload.js +1 -0
  42. package/build/_private/photoRequestPayload.js.map +1 -1
  43. package/build/index.d.ts.map +1 -1
  44. package/build/index.js +4 -0
  45. package/build/index.js.map +1 -1
  46. package/expo-module.config.json +1 -0
  47. package/ios/BluetoothSdkModule.swift +72 -0
  48. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  49. package/ios/Source/Bridge.swift +72 -18
  50. package/ios/Source/DeviceManager.swift +51 -3
  51. package/ios/Source/DeviceStore.swift +1 -1
  52. package/ios/Source/MentraBluetoothSDK.swift +289 -25
  53. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  54. package/ios/Source/camera/CameraModels.swift +8 -3
  55. package/ios/Source/sgcs/G1.swift +1 -1
  56. package/ios/Source/sgcs/G2.swift +19 -7
  57. package/ios/Source/sgcs/Mach1.swift +1 -1
  58. package/ios/Source/sgcs/MentraLive.swift +318 -37
  59. package/ios/Source/sgcs/MentraNex.swift +1 -1
  60. package/ios/Source/sgcs/Nimo.swift +1 -1
  61. package/ios/Source/sgcs/SGCManager.swift +5 -2
  62. package/ios/Source/sgcs/Simulated.swift +1 -1
  63. package/ios/Source/status/WifiHotspotStatus.swift +21 -2
  64. package/ios/Source/streaming/StreamModels.swift +9 -0
  65. package/ios/Source/tts/TTSTools.swift +1 -1
  66. package/package.json +1 -2
  67. package/src/BluetoothSdk.types.ts +49 -0
  68. package/src/_internal.ts +2 -0
  69. package/src/_private/BluetoothSdkModule.ts +64 -13
  70. package/src/_private/MentraLocalNetworkModule.ts +50 -0
  71. package/src/_private/cameraRequestPayload.ts +1 -0
  72. package/src/_private/photoRequestPayload.ts +2 -3
  73. package/src/index.ts +4 -0
@@ -400,7 +400,10 @@ class Bridge {
400
400
  Bridge.sendTypedMessage("glasses_serial_number", body: body)
401
401
  }
402
402
 
403
- static func sendWifiStatusChange(connected: Bool, ssid: String?, localIp: String?) {
403
+ /// `error` is the glasses' provisioning failure reason (e.g. "connect_timeout",
404
+ /// "connected_to_other_network") when this status is the verdict of a failed
405
+ /// connect attempt; nil for routine link-state updates.
406
+ static func sendWifiStatusChange(connected: Bool, ssid: String?, localIp: String?, error: String? = nil) {
404
407
  guard let status = WifiStatus.fromStoreFields(
405
408
  connected: connected,
406
409
  ssid: ssid,
@@ -408,27 +411,78 @@ class Bridge {
408
411
  ) else {
409
412
  return
410
413
  }
411
- Bridge.sendTypedMessage("wifi_status_change", body: status.values)
414
+ var body = status.values
415
+ if let error {
416
+ body["error"] = error
417
+ }
418
+ Bridge.sendTypedMessage("wifi_status_change", body: body)
412
419
  }
413
420
 
414
- static func updateWifiScanResults(_ networks: [[String: Any]], scanComplete: Bool) {
415
- Task {
416
- await MainActor.run {
417
- var storedNetworks: [[String: Any]] =
418
- DeviceStore.shared.get("bluetooth", "wifiScanResults") as? [[String: Any]] ?? []
419
- // add the networks to the storedNetworks array, removing duplicates by ssid
420
- for network in networks {
421
- if !storedNetworks.contains(where: {
422
- $0["ssid"] as? String == network["ssid"] as? String
423
- }) {
424
- storedNetworks.append(network)
421
+ /// Claim the WiFi scan-results store for a newly requested scan. Called by the
422
+ /// SDK when it generates the scanId, BEFORE the scan command goes out: store
423
+ /// ownership is decided at request time, not by whichever chunk arrives first,
424
+ /// so a delayed chunk from an older, abandoned scan can never reset or clobber
425
+ /// the current scan's accumulator.
426
+ @MainActor
427
+ static func claimWifiScanResults(scanId: String) {
428
+ DeviceStore.shared.apply("bluetooth", "wifiScanActiveScanId", scanId)
429
+ }
430
+
431
+ static func updateWifiScanResults(
432
+ _ networks: [[String: Any]],
433
+ scanComplete: Bool,
434
+ scanId: String? = nil
435
+ ) {
436
+ // Correlated scans accumulate chunks until the terminal scan_complete, so
437
+ // chunks must reach the SDK in receive order. A Task per message can reach
438
+ // the MainActor out of creation order; DispatchQueue.main keeps the FIFO
439
+ // order of the serial bluetooth queue that delivers these.
440
+ DispatchQueue.main.async {
441
+ MainActor.assumeIsolated {
442
+ // Only chunks echoing the active scanId claimed at request time may
443
+ // mutate the store; foreign chunks are still forwarded to the SDK
444
+ // sink, which drops stale ids itself. Scan-id-less chunks (old
445
+ // firmware) keep the legacy accumulate-forever store behavior.
446
+ let ownsStore: Bool
447
+ if let scanId {
448
+ ownsStore =
449
+ scanId == DeviceStore.shared.get("bluetooth", "wifiScanActiveScanId") as? String
450
+ } else {
451
+ ownsStore = true
452
+ }
453
+ var updatedNetworks: [[String: Any]] = networks
454
+ if ownsStore {
455
+ var storedNetworks: [[String: Any]] =
456
+ DeviceStore.shared.get("bluetooth", "wifiScanResults") as? [[String: Any]] ?? []
457
+ if let scanId,
458
+ scanId != DeviceStore.shared.get("bluetooth", "wifiScanResultsScanId") as? String
459
+ {
460
+ // First chunk of a new scan: drop networks accumulated for a previous scan
461
+ // so stale entries never carry over into this scan's store.
462
+ storedNetworks = []
463
+ DeviceStore.shared.apply("bluetooth", "wifiScanResultsScanId", scanId)
464
+ }
465
+ // add the networks to the storedNetworks array, removing duplicates by ssid
466
+ for network in networks {
467
+ if !storedNetworks.contains(where: {
468
+ $0["ssid"] as? String == network["ssid"] as? String
469
+ }) {
470
+ storedNetworks.append(network)
471
+ }
425
472
  }
473
+ DeviceStore.shared.apply("bluetooth", "wifiScanResults", storedNetworks)
474
+ updatedNetworks = storedNetworks
426
475
  }
427
- DeviceStore.shared.apply("bluetooth", "wifiScanResults", storedNetworks)
428
- Bridge.sendTypedMessage(
429
- "wifi_scan_result",
430
- body: ["networks": storedNetworks, "scanComplete": scanComplete]
431
- )
476
+ // Correlated scans: the SDK accumulates and dedupes chunks per scanId itself,
477
+ // so forward only this chunk; the merged store list is for UI consumers.
478
+ var body: [String: Any] = [
479
+ "networks": scanId != nil ? networks : updatedNetworks,
480
+ "scanComplete": scanComplete,
481
+ ]
482
+ if let scanId {
483
+ body["scanId"] = scanId
484
+ }
485
+ Bridge.sendTypedMessage("wifi_scan_result", body: body)
432
486
  }
433
487
  }
434
488
  }
@@ -799,6 +799,11 @@ struct ViewState {
799
799
  return
800
800
  }
801
801
 
802
+ if sgc?.isMicSuspendedForAudio == true {
803
+ Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
804
+ return
805
+ }
806
+
802
807
  let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
803
808
  if timeSinceLastLc3Event > 5 {
804
809
  Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
@@ -1276,10 +1281,10 @@ struct ViewState {
1276
1281
  sgc?.sendStreamKeepAlive(message)
1277
1282
  }
1278
1283
 
1279
- func requestWifiScan() {
1284
+ func requestWifiScan(scanId: String? = nil) {
1280
1285
  Bridge.log("MAN: Requesting wifi scan")
1281
1286
  DeviceStore.shared.apply("bluetooth", "wifiScanResults", [])
1282
- sgc?.requestWifiScan()
1287
+ sgc?.requestWifiScan(scanId: scanId)
1283
1288
  }
1284
1289
 
1285
1290
  func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
@@ -1359,6 +1364,42 @@ struct ViewState {
1359
1364
  try liveSgc().sendCameraFovSetting(requestId: requestId, fov: fov, roiPosition: roiPosition)
1360
1365
  }
1361
1366
 
1367
+ func sendLegacyCameraFovSetting(fov: Int, roiPosition: Int) throws {
1368
+ let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
1369
+ guard glassesConnected else {
1370
+ throw BluetoothSdkError(code: "not_connected", message: "Mentra Live glasses are not connected.")
1371
+ }
1372
+ try liveSgc().sendCameraFovSetting(requestId: nil, fov: fov, roiPosition: roiPosition)
1373
+ }
1374
+
1375
+ func restoreLegacyCameraFovSetting() throws {
1376
+ let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
1377
+ guard glassesConnected else {
1378
+ throw BluetoothSdkError(code: "not_connected", message: "Mentra Live glasses are not connected.")
1379
+ }
1380
+ try liveSgc().sendCameraFovSetting()
1381
+ }
1382
+
1383
+ func sendCameraFovOverride(
1384
+ requestId: String,
1385
+ leaseId: String,
1386
+ fov: Int,
1387
+ roiPosition: Int,
1388
+ ttlMs: Int
1389
+ ) throws {
1390
+ try liveSgc().sendCameraFovOverride(
1391
+ requestId: requestId,
1392
+ leaseId: leaseId,
1393
+ fov: fov,
1394
+ roiPosition: roiPosition,
1395
+ ttlMs: ttlMs
1396
+ )
1397
+ }
1398
+
1399
+ func releaseCameraFovOverride(requestId: String, leaseId: String) throws {
1400
+ try liveSgc().releaseCameraFovOverride(requestId: requestId, leaseId: leaseId)
1401
+ }
1402
+
1362
1403
  func sendCameraTuningConfig(requestId: String, anrOn: Bool, gainOn: Bool) throws {
1363
1404
  try liveSgc().sendCameraTuningConfig(requestId: requestId, anrOn: anrOn, gainOn: gainOn)
1364
1405
  }
@@ -1366,6 +1407,7 @@ struct ViewState {
1366
1407
  func warmUpCamera(
1367
1408
  requestId: String,
1368
1409
  size: PhotoSize,
1410
+ mode: PhotoMode = .photo,
1369
1411
  exposureTimeNs: Double?,
1370
1412
  durationMs: Int
1371
1413
  ) throws {
@@ -1378,11 +1420,16 @@ struct ViewState {
1378
1420
  live.warmUpCamera(
1379
1421
  requestId: requestId,
1380
1422
  size: size,
1423
+ mode: mode,
1381
1424
  exposureTimeNs: exposureTimeNs,
1382
1425
  durationMs: durationMs
1383
1426
  )
1384
1427
  }
1385
1428
 
1429
+ func stopCameraWarmUp(requestId: String) throws {
1430
+ try liveSgc().stopCameraWarmUp(requestId: requestId)
1431
+ }
1432
+
1386
1433
  /// Request version info from glasses.
1387
1434
  /// Glasses will respond with version_info message containing build number, firmware version, etc.
1388
1435
  func requestVersionInfo() {
@@ -1506,7 +1553,8 @@ struct ViewState {
1506
1553
  zsl: request.zsl,
1507
1554
  ispDigitalGain: request.ispDigitalGain,
1508
1555
  ispAnalogGain: request.ispAnalogGain,
1509
- mode: request.mode
1556
+ mode: request.mode,
1557
+ transferMethod: request.transferMethod
1510
1558
  )
1511
1559
  Bridge.log(
1512
1560
  "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")"
@@ -93,7 +93,7 @@ class DeviceStore {
93
93
  store.set("bluetooth", "screen_disabled", false)
94
94
  store.set("bluetooth", "button_photo_size", "max")
95
95
  store.set("bluetooth", "button_max_recording_time", 10)
96
- store.set("bluetooth", "camera_fov", ["fov": 118, "roi_position": 0])
96
+ store.set("bluetooth", "camera_fov", ["fov": 102, "roi_position": 0])
97
97
  store.set("bluetooth", "button_video_width", 1280)
98
98
  store.set("bluetooth", "button_video_height", 720)
99
99
  store.set("bluetooth", "button_video_fps", 30)