@glassly/bluetooth-sdk 0.1.0-dev.98 → 0.2.8-dev.444

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 (155) hide show
  1. package/README.md +47 -6
  2. package/android/build.gradle +7 -7
  3. package/android/lc3Lib/build.gradle +2 -2
  4. package/android/src/main/assets/G2Firmware/README.md +27 -2
  5. package/android/src/main/assets/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
  6. package/android/src/main/assets/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
  7. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
  8. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
  9. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
  10. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
  11. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
  12. package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
  13. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
  14. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
  15. package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
  16. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1075 -257
  17. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
  18. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
  19. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
  20. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
  21. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
  22. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
  23. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
  24. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
  25. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
  26. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
  27. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
  28. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
  29. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
  30. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
  31. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
  32. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
  33. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
  34. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
  35. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
  36. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
  37. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
  38. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
  39. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
  40. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
  41. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
  42. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
  43. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
  44. package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
  45. package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
  46. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
  47. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
  48. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
  49. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
  50. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
  51. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
  52. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
  53. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
  54. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
  55. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
  56. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
  57. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
  58. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
  59. package/build/BluetoothSdk.types.d.ts +209 -20
  60. package/build/BluetoothSdk.types.d.ts.map +1 -1
  61. package/build/BluetoothSdk.types.js.map +1 -1
  62. package/build/_internal.d.ts +1 -1
  63. package/build/_internal.d.ts.map +1 -1
  64. package/build/_internal.js.map +1 -1
  65. package/build/_private/BluetoothSdkModule.d.ts +30 -1
  66. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  67. package/build/_private/BluetoothSdkModule.js.map +1 -1
  68. package/build/generated/changelogCatalog.d.ts +28 -1
  69. package/build/generated/changelogCatalog.d.ts.map +1 -1
  70. package/build/generated/changelogCatalog.js +37 -1
  71. package/build/generated/changelogCatalog.js.map +1 -1
  72. package/build/generated/releaseMetadata.js +4 -4
  73. package/build/generated/releaseMetadata.js.map +1 -1
  74. package/build/index.d.ts +1 -1
  75. package/build/index.d.ts.map +1 -1
  76. package/build/index.js +14 -0
  77. package/build/index.js.map +1 -1
  78. package/ios/BluetoothSdkModule.swift +102 -13
  79. package/ios/GlasslyBluetoothSDK.podspec +1 -1
  80. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
  81. package/ios/Source/BluetoothSDK.swift +53 -0
  82. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  83. package/ios/Source/Bridge.swift +13 -0
  84. package/ios/Source/DeviceManager.swift +151 -33
  85. package/ios/Source/DeviceStore.swift +28 -14
  86. package/ios/Source/DisplayPacer.swift +180 -0
  87. package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
  88. package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
  89. package/ios/Source/GlassesNotifications.swift +181 -0
  90. package/ios/Source/ObservableStore.swift +14 -2
  91. package/ios/Source/Resources/G2Firmware/README.md +27 -2
  92. package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
  93. package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
  94. package/ios/Source/controllers/R1.swift +0 -1
  95. package/ios/Source/internal/BluetoothAvailability.swift +31 -0
  96. package/ios/Source/sgcs/G2.swift +1434 -504
  97. package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
  98. package/ios/Source/sgcs/G2BleLink.swift +102 -0
  99. package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
  100. package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
  101. package/ios/Source/sgcs/G2CfwScene.swift +779 -0
  102. package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
  103. package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
  104. package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
  105. package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
  106. package/ios/Source/sgcs/G2Flasher.swift +9 -9
  107. package/ios/Source/sgcs/G2Keyboard.swift +780 -0
  108. package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
  109. package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
  110. package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
  111. package/ios/Source/sgcs/G2RingBattery.swift +22 -0
  112. package/ios/Source/sgcs/G2Sensors.swift +75 -0
  113. package/ios/Source/sgcs/G2TouchInput.swift +51 -0
  114. package/ios/Source/sgcs/G2WritePacing.swift +18 -0
  115. package/ios/Source/sgcs/MentraNex.swift +44 -44
  116. package/ios/Source/sgcs/SGCManager.swift +120 -13
  117. package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
  118. package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
  119. package/ios/Source/status/DeviceStatus.swift +27 -0
  120. package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
  121. package/ios/Source/utils/Enums.swift +0 -1
  122. package/ios/Source/utils/G1Text.swift +0 -1
  123. package/ios/Source/utils/G2NotifyFile.swift +20 -10
  124. package/ios/Source/utils/JSCExperiment.swift +6 -6
  125. package/ios/Source/utils/Models.swift +0 -1
  126. package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
  127. package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
  128. package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
  129. package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
  130. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
  131. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
  132. package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
  133. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
  134. package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
  135. package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
  136. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
  137. package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
  138. package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
  139. package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
  140. package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
  141. package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
  142. package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
  143. package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
  144. package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
  145. package/package.json +1 -1
  146. package/plugin/build/withAndroid.js +2 -2
  147. package/plugin/build/withIos.js +1 -1
  148. package/scripts/gen-nex-proto.sh +12 -0
  149. package/scripts/inject-ios-sdk-version.mjs +1 -1
  150. package/src/BluetoothSdk.types.ts +214 -19
  151. package/src/_internal.ts +1 -1
  152. package/src/_private/BluetoothSdkModule.ts +40 -0
  153. package/src/generated/changelogCatalog.ts +37 -1
  154. package/src/generated/releaseMetadata.ts +4 -4
  155. package/src/index.ts +24 -0
@@ -17,6 +17,7 @@ import com.glassly.bluetoothsdk.services.PhoneMic
17
17
  import com.glassly.bluetoothsdk.sgcs.Ar99
18
18
  import com.glassly.bluetoothsdk.sgcs.G1
19
19
  import com.glassly.bluetoothsdk.sgcs.G2
20
+ import com.glassly.bluetoothsdk.sgcs.G2KeyboardBridge
20
21
  import com.glassly.bluetoothsdk.sgcs.SceneElement
21
22
  import com.glassly.bluetoothsdk.sgcs.SceneFrame
22
23
  import com.glassly.bluetoothsdk.sgcs.Mach1
@@ -33,6 +34,7 @@ import com.glassly.bluetoothsdk.utils.PhoneAudioMonitor
33
34
  import com.glassly.lc3Lib.Lc3Cpp
34
35
  import com.glassly.bluetoothsdk.speakerid.SpeakerIdEngine
35
36
  import com.glassly.bluetoothsdk.stt.SherpaOnnxTranscriber
37
+ import com.glassly.bluetoothsdk.stt.VadGateSpeechPolicy
36
38
  import kotlinx.coroutines.CoroutineScope
37
39
  import kotlinx.coroutines.Dispatchers
38
40
  import kotlinx.coroutines.launch
@@ -267,7 +269,7 @@ class DeviceManager {
267
269
  // VAD
268
270
  private val vadBuffer = mutableListOf<ByteArray>()
269
271
  private var isSpeaking = false
270
- private var vadPolicy: com.glassly.bluetoothsdk.stt.VadGateSpeechPolicy? = null
272
+ private var vadPolicy: VadGateSpeechPolicy? = null
271
273
 
272
274
  // STT
273
275
  private var transcriber: SherpaOnnxTranscriber? = null
@@ -306,7 +308,7 @@ class DeviceManager {
306
308
  try {
307
309
  vadPolicy?.stop()
308
310
  val ctx = Bridge.getContext()
309
- val policy = com.glassly.bluetoothsdk.stt.VadGateSpeechPolicy(ctx)
311
+ val policy = VadGateSpeechPolicy(ctx)
310
312
  policy.init(blockSizeSamples = 512)
311
313
  policy.onSpeechStateChanged = { speaking ->
312
314
  isSpeaking = speaking
@@ -703,6 +705,27 @@ class DeviceManager {
703
705
  // native re-dispatch coherent (nav exit re-applies a complete scene, not
704
706
  // whatever element happened to arrive last).
705
707
  private var sceneState: SceneFrame? = null
708
+
709
+ /**
710
+ * Paces scene frames and legacy layouts to display_max_fps; clears bypass it.
711
+ * CFW scene drivers coalesce off the link themselves, so they bypass it too.
712
+ */
713
+ private val pacer =
714
+ DisplayPacer(dispatchScene = { dispatchSceneFrame(it) }, sendLegacy = { sendCurrentState() }).also {
715
+ it.setMaxFps((DeviceStore.store.get("bluetooth", "display_max_fps") as? Number)?.toInt() ?: DisplayPacer.DEFAULT_FPS)
716
+ }
717
+
718
+ fun setDisplayMaxFps(fps: Int) = pacer.setMaxFps(fps)
719
+
720
+ /**
721
+ * CFW identity can land after device-ready, so this is re-read per update;
722
+ * the flush variants fold any frame already held into the immediate send.
723
+ */
724
+ private val pacerBypassed: Boolean
725
+ get() = sgc?.supportsCfwSceneRendering == true
726
+
727
+ /** Drop paced display work (screen off): the retained state repaints on re-enable. */
728
+ fun cancelPendingDisplay() = pacer.cancel()
706
729
  // MARK: - End Unique
707
730
 
708
731
  // MARK: - Voice Data Handling
@@ -1208,6 +1231,7 @@ class DeviceManager {
1208
1231
  if (sgc != null && sgc?.type != wearable) {
1209
1232
  Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
1210
1233
  Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
1234
+ pacer.cancel()
1211
1235
  sgc?.cleanup()
1212
1236
  sgc = null
1213
1237
  resetSystemTimeSync()
@@ -1276,9 +1300,10 @@ class DeviceManager {
1276
1300
 
1277
1301
  fun restartTranscriber() {
1278
1302
  Bridge.log("MAN: Restarting transcriber via command")
1279
- if (ensureTranscriberInitialized()) {
1280
- transcriber?.restart()
1281
- }
1303
+ check(ensureTranscriberInitialized()) { "Offline speech recognizer initialization failed" }
1304
+ val activeTranscriber = checkNotNull(transcriber)
1305
+ activeTranscriber.restart()
1306
+ check(activeTranscriber.isInitialized()) { "Offline speech recognizer restart failed" }
1282
1307
  }
1283
1308
 
1284
1309
  // MARK: - Speaker identification
@@ -1580,6 +1605,7 @@ class DeviceManager {
1580
1605
 
1581
1606
  fun clearDisplay() {
1582
1607
  Bridge.log("MAN: Clearing Display")
1608
+ pacer.cancel()
1583
1609
  sgc?.clearDisplay()
1584
1610
  }
1585
1611
 
@@ -1647,7 +1673,11 @@ class DeviceManager {
1647
1673
  }
1648
1674
 
1649
1675
  viewState = newViewState
1650
- sendCurrentState()
1676
+ if (layoutType == "clear_view" || pacerBypassed) {
1677
+ pacer.flushLegacy()
1678
+ } else {
1679
+ pacer.submitLegacy()
1680
+ }
1651
1681
  }
1652
1682
 
1653
1683
  /** Parse + store a scene frame, then dispatch it. */
@@ -1682,15 +1712,28 @@ class DeviceManager {
1682
1712
  frame.copy(replay = true, elements = frame.elements.map { it.copy(change = "created") })
1683
1713
  viewState = ViewState(" ", " ", " ", "scene", " ", null, null)
1684
1714
 
1685
- dispatchSceneFrame(frame)
1715
+ if (frame.elements.isEmpty() || pacerBypassed) {
1716
+ pacer.flushScene(frame)
1717
+ } else {
1718
+ pacer.submitScene(frame)
1719
+ }
1686
1720
  }
1687
1721
 
1688
1722
  /** Guarded scene dispatch — mirrors sendCurrentState's send conditions. */
1689
1723
  private fun dispatchSceneFrame(frame: SceneFrame) {
1690
- if (screenDisabled) return
1691
- if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) return
1724
+ // Each early-out tells the host the frame won't show now; the SGC
1725
+ // reports the rest once the device has it.
1726
+ if (screenDisabled) {
1727
+ frame.reportPresented(false, "screen off")
1728
+ return
1729
+ }
1730
+ if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) {
1731
+ frame.reportPresented(false, "simulated glasses")
1732
+ return
1733
+ }
1692
1734
  if (sgc?.fullyBooted != true) {
1693
1735
  Bridge.log("MAN: dispatchSceneFrame(): sgc not ready")
1736
+ frame.reportPresented(false, if (sgc == null) "no glasses" else "glasses not ready")
1694
1737
  return
1695
1738
  }
1696
1739
  sgc?.applySceneFrame(frame)
@@ -1703,6 +1746,15 @@ class DeviceManager {
1703
1746
  elementsRaw.mapNotNull { el ->
1704
1747
  val box = el["box"] as? Map<String, Any> ?: return@mapNotNull null
1705
1748
  val style = el["style"] as? Map<String, Any>
1749
+ val transition = el["transition"] as? Map<String, Any>
1750
+ val points =
1751
+ (el["points"] as? List<*>)?.flatMap { p ->
1752
+ val point = p as? Map<*, *>
1753
+ listOf((point?.get("x") as? Number)?.toInt() ?: 0, (point?.get("y") as? Number)?.toInt() ?: 0)
1754
+ } ?: emptyList()
1755
+ // Open strokes carry their width as `width`; closed shapes and boxes as `border`.
1756
+ val border =
1757
+ (style?.get("border") as? Number)?.toInt() ?: (style?.get("width") as? Number)?.toInt() ?: 0
1706
1758
  SceneElement(
1707
1759
  id = el["id"] as? String ?: return@mapNotNull null,
1708
1760
  type = el["type"] as? String ?: return@mapNotNull null,
@@ -1712,15 +1764,30 @@ class DeviceManager {
1712
1764
  h = (box["h"] as? Number)?.toInt() ?: 0,
1713
1765
  text = (el["text"] as? String)?.let { parsePlaceholders(it) },
1714
1766
  data = el["data"] as? String,
1715
- border = (style?.get("border") as? Number)?.toInt() ?: 0,
1767
+ border = border,
1716
1768
  radius = (style?.get("radius") as? Number)?.toInt() ?: 0,
1717
1769
  change = el["change"] as? String ?: "created",
1718
1770
  contentHash = el["contentHash"] as? String ?: "",
1719
1771
  font = style?.get("font") as? String ?: "default",
1720
1772
  fontSize = (style?.get("fontSize") as? Number)?.toInt() ?: 0,
1773
+ fill = style?.get("fill") as? Boolean ?: false,
1774
+ color = (style?.get("color") as? Number)?.toInt() ?: -1,
1775
+ points = points,
1776
+ startDeg = (el["startDeg"] as? Number)?.toInt() ?: 0,
1777
+ endDeg = (el["endDeg"] as? Number)?.toInt() ?: 0,
1778
+ transitionMs = (transition?.get("durationMs") as? Number)?.toInt() ?: 0,
1779
+ easing = transition?.get("easing") as? String ?: "ease",
1780
+ path = (el["path"] as? List<*>)?.map { (it as? Number)?.toInt()?.coerceIn(0, 255) ?: 0 } ?: emptyList(),
1781
+ pathScale = (el["pathScale"] as? Number)?.toInt() ?: 256,
1782
+ fillRule = if (el["fillRule"] == "evenodd") 1 else 0,
1783
+ rotationQ8 = (((el["rotation"] as? Number)?.toDouble() ?: 0.0) * 256).coerceIn(-9216000.0, 9216000.0).toInt(),
1784
+ pivotX = ((el["pivot"] as? Map<*, *>)?.get("x") as? Number)?.toInt(),
1785
+ pivotY = ((el["pivot"] as? Map<*, *>)?.get("y") as? Number)?.toInt(),
1786
+ depth = (el["depth"] as? Number)?.toInt() ?: 0,
1721
1787
  )
1722
1788
  }
1723
1789
  val canvas = sceneMap["canvas"] as? Map<String, Any>
1790
+
1724
1791
  return SceneFrame(
1725
1792
  appId = sceneMap["appId"] as? String ?: "",
1726
1793
  epoch = (sceneMap["sceneEpoch"] as? Number)?.toInt() ?: 0,
@@ -1731,6 +1798,7 @@ class DeviceManager {
1731
1798
  ?: SceneFrame.DEFAULT_CANVAS_WIDTH,
1732
1799
  canvasHeight = (canvas?.get("height") as? Number)?.toInt()?.takeIf { it > 0 }
1733
1800
  ?: SceneFrame.DEFAULT_CANVAS_HEIGHT,
1801
+ seq = (sceneMap["seq"] as? Number)?.toInt() ?: 0,
1734
1802
  )
1735
1803
  }
1736
1804
 
@@ -1740,6 +1808,9 @@ class DeviceManager {
1740
1808
 
1741
1809
  // ---------- Native navigation (glasses-rendered nav UI, e.g. G2 service 0x08) ----------
1742
1810
 
1811
+ suspend fun glassesSensorControl(sensor: String, operation: String, options: Map<String, Double>): Boolean =
1812
+ (sgc as? G2)?.glassesSensorControl(sensor, operation, options) ?: false
1813
+
1743
1814
  fun navigationModeStart() {
1744
1815
  sgc?.enterNavigationMode()
1745
1816
  }
@@ -1792,6 +1863,19 @@ class DeviceManager {
1792
1863
  }
1793
1864
  }
1794
1865
 
1866
+ fun setGlassesNotificationConfig(config: GlassesNotificationConfig) {
1867
+ val manager = sgc ?: throw IllegalStateException("Glasses are not connected")
1868
+ manager.setGlassesNotificationConfig(config)
1869
+ }
1870
+
1871
+ fun glassesNotificationStatus(): GlassesNotificationStatus =
1872
+ sgc?.glassesNotificationStatus() ?: GlassesNotificationStatus.unsupported()
1873
+
1874
+ fun uploadPhoneNotification(upload: PhoneNotificationUpload) {
1875
+ val manager = sgc ?: throw IllegalStateException("Glasses are not connected")
1876
+ manager.uploadPhoneNotification(upload)
1877
+ }
1878
+
1795
1879
  fun ping() {
1796
1880
  sgc?.ping()
1797
1881
  }
@@ -1806,6 +1890,16 @@ class DeviceManager {
1806
1890
  sgc?.dbg2()
1807
1891
  }
1808
1892
 
1893
+ fun dbg3() {
1894
+ Bridge.log("MAN: dbg3()")
1895
+ sgc?.dbg3()
1896
+ }
1897
+
1898
+ fun dbg4() {
1899
+ Bridge.log("MAN: dbg4()")
1900
+ sgc?.dbg4()
1901
+ }
1902
+
1809
1903
  fun startStream(message: MutableMap<String, Any>) {
1810
1904
  Bridge.log("MAN: startStream")
1811
1905
  sgc?.startStream(message)
@@ -2066,6 +2160,40 @@ class DeviceManager {
2066
2160
  return g2.startFirmwareFlash(target)
2067
2161
  }
2068
2162
 
2163
+ // ---------- BLE keyboard bridge (G2 Glassly CFW revision 39+) ----------
2164
+
2165
+ private fun requireG2ForKeyboard(): G2 =
2166
+ sgc as? G2 ?: throw G2KeyboardBridge.BridgeException("The keyboard bridge needs connected G2 glasses.")
2167
+
2168
+ suspend fun g2KeyboardScan(): List<Map<String, Any>> = requireG2ForKeyboard().keyboardScan()
2169
+
2170
+ suspend fun g2KeyboardConnect(address: String, addressType: Int, name: String) =
2171
+ requireG2ForKeyboard().keyboardConnect(address, addressType, name)
2172
+
2173
+ suspend fun g2KeyboardReconnect() = requireG2ForKeyboard().keyboardReconnect()
2174
+
2175
+ suspend fun g2KeyboardDisconnect() {
2176
+ (sgc as? G2)?.keyboardDisconnect()
2177
+ }
2178
+
2179
+ suspend fun g2KeyboardForget() {
2180
+ val g2 = sgc as? G2
2181
+ if (g2 != null) {
2182
+ g2.keyboardForget()
2183
+ } else {
2184
+ // No live G2 driver: drop the remembered identity directly.
2185
+ G2KeyboardBridge.forgetSaved(Bridge.getContext().getSharedPreferences("G2Prefs", Context.MODE_PRIVATE))
2186
+ }
2187
+ }
2188
+
2189
+ suspend fun g2KeyboardPasskey(pin: Int) = requireG2ForKeyboard().keyboardPasskey(pin)
2190
+
2191
+ suspend fun g2KeyboardCompare(accept: Boolean) = requireG2ForKeyboard().keyboardCompare(accept)
2192
+
2193
+ suspend fun g2KeyboardStatus(): Map<String, Any> =
2194
+ (sgc as? G2)?.keyboardStatus()
2195
+ ?: mapOf("keyboardBridge" to false, "keyboardConnected" to false, "keyboardState" to "idle")
2196
+
2069
2197
  /** Send shutdown command to glasses. This will initiate a graceful shutdown of the device. */
2070
2198
  fun sendShutdown() {
2071
2199
  Bridge.log("MAN: 🔌 Sending shutdown command to glasses")
@@ -2280,6 +2408,7 @@ class DeviceManager {
2280
2408
  }
2281
2409
 
2282
2410
  fun disconnect(clearPendingIdentity: Boolean = true) {
2411
+ pacer.cancel()
2283
2412
  sgc?.clearDisplay()
2284
2413
  sgc?.disconnect()
2285
2414
  sgc = null // Clear the SGC reference after disconnect
@@ -72,8 +72,19 @@ object DeviceStore {
72
72
  store.set("glasses", "caseRemoved", true)
73
73
  store.set("glasses", "caseOpen", true)
74
74
  store.set("glasses", "caseCharging", false)
75
+ store.set("glasses", "ringConnected", false)
76
+ store.set("glasses", "ringBatteryLevel", -1)
77
+ store.set("glasses", "ringCharging", false)
75
78
  store.set("glasses", "caseBatteryLevel", -1)
76
79
  store.set("glasses", "headUp", false)
80
+ // G2 Glassly CFW keyboard bridge (revision 39+): availability on the
81
+ // connected firmware, the live keyboard link, and the remembered keyboard.
82
+ store.set("glasses", "keyboardBridge", false)
83
+ store.set("glasses", "keyboardConnected", false)
84
+ store.set("glasses", "keyboardName", "")
85
+ store.set("glasses", "keyboardSaved", false)
86
+ store.set("glasses", "keyboardState", "idle")
87
+ store.set("glasses", "keyboardError", "")
77
88
  store.set("glasses", "bluetoothMacAddress", "")
78
89
  store.set("glasses", "wifiMacAddress", "")
79
90
  store.set("glasses", "leftMacAddress", "")
@@ -132,6 +143,9 @@ object DeviceStore {
132
143
  store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
133
144
  store.set("bluetooth", "loudness_gate_enabled", BluetoothSdkDefaults.LOUDNESS_GATE_ENABLED)
134
145
  store.set("bluetooth", "screen_disabled", false)
146
+ store.set("bluetooth", "display_max_fps", DisplayPacer.DEFAULT_FPS)
147
+ // Debug: G2 CFW fast BLE link profile (off = stock connection behaviour).
148
+ store.set("bluetooth", "cfw_fast_ble", false)
135
149
  store.set("bluetooth", "button_photo_size", "max")
136
150
  store.set("bluetooth", "button_max_recording_time", 10)
137
151
  store.set("bluetooth", "camera_fov", mapOf("fov" to 118, "roi_position" to 0))
@@ -145,7 +159,6 @@ object DeviceStore {
145
159
  store.set("bluetooth", "should_send_pcm", false)
146
160
  store.set("bluetooth", "should_send_lc3", false)
147
161
  store.set("bluetooth", "should_send_transcript", false)
148
- store.set("bluetooth", "use_native_dashboard", false)
149
162
  // Mentra Nex feature flags (off by default; toggled from Nex Developer Settings):
150
163
  store.set("bluetooth", "nex_chinese_captions", false)
151
164
  store.set("bluetooth", "nex_lc3_audio_playback", false)
@@ -241,6 +254,19 @@ object DeviceStore {
241
254
  }
242
255
  }
243
256
  }
257
+ "bluetooth" to "cfw_fast_ble" -> {
258
+ val enabled = value as? Boolean
259
+ if (enabled != null && (oldValue as? Boolean) != enabled) {
260
+ CoroutineScope(Dispatchers.Main).launch {
261
+ DeviceManager.getInstance().sgc?.cfwFastBleToggled(enabled)
262
+ }
263
+ }
264
+ }
265
+ "bluetooth" to "display_max_fps" -> {
266
+ (value as? Number)?.let { fps ->
267
+ DeviceManager.getInstance().setDisplayMaxFps(fps.toInt())
268
+ }
269
+ }
244
270
  "bluetooth" to "dashboard_height" -> {
245
271
  scheduleDashboardHeightToGlasses()
246
272
  }
@@ -294,6 +320,7 @@ object DeviceStore {
294
320
  "bluetooth" to "screen_disabled" -> {
295
321
  (value as? Boolean)?.let { disabled ->
296
322
  if (disabled) {
323
+ DeviceManager.getInstance().cancelPendingDisplay()
297
324
  DeviceManager.getInstance().sgc?.exit()
298
325
  } else {
299
326
  DeviceManager.getInstance().sgc?.clearDisplay()
@@ -0,0 +1,199 @@
1
+ package com.glassly.bluetoothsdk
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import com.glassly.bluetoothsdk.sgcs.SceneFrame
6
+
7
+ /**
8
+ * Paces display updates leaving the phone to at most [setMaxFps] per second.
9
+ *
10
+ * Updates that arrive inside the current slot are held and sent at the next
11
+ * one, last-wins: a newer scene frame replaces a pending one (with the pending
12
+ * frame's per-element annotations folded in so per-element devices never miss
13
+ * a change), a legacy layout replaces a pending scene and vice versa. Clears
14
+ * ([flushScene] with an empty frame, [flushLegacy]) bypass the pacer and drop
15
+ * pending work.
16
+ *
17
+ * Safe to call from any thread; the trailing send runs on the scheduler's
18
+ * thread (main looper by default). Sinks are always invoked outside the lock.
19
+ */
20
+ class DisplayPacer(
21
+ private val dispatchScene: (SceneFrame) -> Unit,
22
+ private val sendLegacy: () -> Unit,
23
+ private val clock: () -> Long = { System.nanoTime() / 1_000_000 },
24
+ private val scheduler: Scheduler = MainScheduler(),
25
+ ) {
26
+ /** Runs [block] after [delayMs]; the returned function cancels it. */
27
+ fun interface Scheduler {
28
+ fun schedule(delayMs: Long, block: () -> Unit): () -> Unit
29
+ }
30
+
31
+ private class MainScheduler : Scheduler {
32
+ private val handler = Handler(Looper.getMainLooper())
33
+
34
+ override fun schedule(delayMs: Long, block: () -> Unit): () -> Unit {
35
+ val runnable = Runnable { block() }
36
+ handler.postDelayed(runnable, delayMs)
37
+ return { handler.removeCallbacks(runnable) }
38
+ }
39
+ }
40
+
41
+ companion object {
42
+ const val MIN_FPS = 2
43
+ const val MAX_FPS = 20
44
+ const val DEFAULT_FPS = 8
45
+
46
+ fun intervalFor(fps: Int): Long = 1000L / fps.coerceIn(MIN_FPS, MAX_FPS)
47
+
48
+ /**
49
+ * Fold pending frame [previous] into [next] (both unsent). The device
50
+ * only ever sees the merged frame, so an element the pending frame
51
+ * changed must not read as "unchanged" in the merged one, and ids the
52
+ * pending frame removed must still be removed unless [next] re-creates
53
+ * them. A different app is already a from-scratch replay.
54
+ */
55
+ fun merge(previous: SceneFrame?, next: SceneFrame): SceneFrame {
56
+ if (previous == null || previous.appId != next.appId) return next
57
+ val pending = previous.elements.associateBy { it.id }
58
+ val elements =
59
+ next.elements.map { el ->
60
+ val prior = pending[el.id] ?: return@map el
61
+ when {
62
+ prior.change == "created" -> el.copy(change = "created")
63
+ prior.change != "unchanged" && el.change == "unchanged" -> el.copy(change = "updated")
64
+ else -> el
65
+ }
66
+ }
67
+ val painted = next.elements.mapTo(HashSet()) { it.id }
68
+ val removed = (previous.removed + next.removed).distinct().filter { it !in painted }
69
+ return next.copy(replay = previous.replay || next.replay, elements = elements, removed = removed)
70
+ }
71
+ }
72
+
73
+ private val lock = Any()
74
+ private var intervalMs: Long = intervalFor(DEFAULT_FPS)
75
+ private var lastSendAt: Long = Long.MIN_VALUE / 2
76
+ private var pendingScene: SceneFrame? = null
77
+ private var pendingLegacy = false
78
+ private var cancelTimer: (() -> Unit)? = null
79
+
80
+ fun setMaxFps(fps: Int) {
81
+ var send: (() -> Unit)? = null
82
+ synchronized(lock) {
83
+ intervalMs = intervalFor(fps)
84
+ if (!hasPending()) return
85
+ stopTimer()
86
+ send = armOrTake(clock())
87
+ }
88
+ send?.invoke()
89
+ }
90
+
91
+ /** Send [frame] at the next slot; replaces any pending update. */
92
+ fun submitScene(frame: SceneFrame) {
93
+ var send: (() -> Unit)? = null
94
+ synchronized(lock) {
95
+ val now = clock()
96
+ val merged = merge(pendingScene, frame)
97
+ pendingScene = merged
98
+ pendingLegacy = false
99
+ if (now - lastSendAt >= intervalMs) {
100
+ stopTimer()
101
+ send = take(now)
102
+ } else if (cancelTimer == null) {
103
+ armTimer(now)
104
+ }
105
+ }
106
+ send?.invoke()
107
+ }
108
+
109
+ /** Send the current legacy view state at the next slot; replaces any pending update. */
110
+ fun submitLegacy() {
111
+ var send: (() -> Unit)? = null
112
+ synchronized(lock) {
113
+ val now = clock()
114
+ pendingScene = null
115
+ pendingLegacy = true
116
+ if (now - lastSendAt >= intervalMs) {
117
+ stopTimer()
118
+ send = take(now)
119
+ } else if (cancelTimer == null) {
120
+ armTimer(now)
121
+ }
122
+ }
123
+ send?.invoke()
124
+ }
125
+
126
+ /** Immediate send for an empty (clearing) frame; pending removes ride along. */
127
+ fun flushScene(frame: SceneFrame) {
128
+ val merged: SceneFrame
129
+ synchronized(lock) {
130
+ stopTimer()
131
+ merged = merge(pendingScene, frame)
132
+ pendingScene = null
133
+ pendingLegacy = false
134
+ lastSendAt = clock()
135
+ }
136
+ dispatchScene(merged)
137
+ }
138
+
139
+ /** Immediate legacy send (clear_view); drops any pending scene. */
140
+ fun flushLegacy() {
141
+ synchronized(lock) {
142
+ stopTimer()
143
+ pendingScene = null
144
+ pendingLegacy = false
145
+ lastSendAt = clock()
146
+ }
147
+ sendLegacy()
148
+ }
149
+
150
+ /** Drop pending work without sending (disconnect, screen off, clearDisplay). */
151
+ fun cancel(reason: String = "cancelled") {
152
+ synchronized(lock) {
153
+ stopTimer()
154
+ pendingScene?.reportPresented(false, reason)
155
+ pendingScene = null
156
+ pendingLegacy = false
157
+ }
158
+ }
159
+
160
+ private fun hasPending() = pendingScene != null || pendingLegacy
161
+
162
+ private fun stopTimer() {
163
+ cancelTimer?.invoke()
164
+ cancelTimer = null
165
+ }
166
+
167
+ /** Lock held. Either sends now (returns the send) or arms the slot timer. */
168
+ private fun armOrTake(now: Long): (() -> Unit)? {
169
+ if (now - lastSendAt >= intervalMs) return take(now)
170
+ armTimer(now)
171
+ return null
172
+ }
173
+
174
+ /** Lock held. Consume pending work into a send closure and stamp the slot. */
175
+ private fun take(now: Long): (() -> Unit)? {
176
+ val scene = pendingScene
177
+ val legacy = pendingLegacy
178
+ pendingScene = null
179
+ pendingLegacy = false
180
+ if (scene == null && !legacy) return null
181
+ lastSendAt = now
182
+ return if (scene != null) ({ dispatchScene(scene) }) else ({ sendLegacy() })
183
+ }
184
+
185
+ /** Lock held. Arm the single trailing timer for the next slot. */
186
+ private fun armTimer(now: Long) {
187
+ val delay = (lastSendAt + intervalMs - now).coerceAtLeast(0)
188
+ cancelTimer = scheduler.schedule(delay) { onTimer() }
189
+ }
190
+
191
+ private fun onTimer() {
192
+ var send: (() -> Unit)? = null
193
+ synchronized(lock) {
194
+ cancelTimer = null
195
+ send = take(clock())
196
+ }
197
+ send?.invoke()
198
+ }
199
+ }
@@ -4,6 +4,15 @@ package com.glassly.bluetoothsdk
4
4
  internal val GENERATED_RELEASE_CHANGELOGS: List<ReleaseChangelog> =
5
5
  listOf(
6
6
  ReleaseChangelog(version = "3.2.0", markdown = "This release is under active development. User-facing changes will be documented as they land."),
7
- ReleaseChangelog(version = "3.1.0", markdown = "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- GlasslyOS, Glassly 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."),
7
+ ReleaseChangelog(version = "3.1.0", markdown = "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- Glassly, Glassly 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."),
8
+ ReleaseChangelog(version = "0.2.8", markdown = "This release is under active development. User-facing changes will be documented as they land."),
9
+ ReleaseChangelog(version = "0.2.7", markdown = "This release is under active development. User-facing changes will be documented as they land."),
10
+ ReleaseChangelog(version = "0.2.6", markdown = "- Terminal follows light and dark mode, removes the on-glasses status badge, and uses font-scaled line spacing in the glasses session picker.\n- Notifications uses the explicit 5-second popup default and prevents text selection on iOS.\n- The session.display example removes the SVG and rotation demo."),
11
+ ReleaseChangelog(version = "0.2.5", markdown = "This release is under active development. User-facing changes will be documented as they land."),
12
+ ReleaseChangelog(version = "0.2.4", markdown = "This release is under active development. User-facing changes will be documented as they land."),
13
+ ReleaseChangelog(version = "0.2.3", markdown = "This release is under active development. User-facing changes will be documented as they land."),
14
+ ReleaseChangelog(version = "0.2.2", markdown = "This release is under active development. User-facing changes will be documented as they land."),
15
+ ReleaseChangelog(version = "0.2.1", markdown = "This release is under active development. User-facing changes will be documented as they land."),
16
+ ReleaseChangelog(version = "0.2.0", markdown = "The glasses UI on Glassly CFW is now a set of pages over one overlay: the dashboard, side menu, calendar, notification viewer and debug menu each stand on their own.\n\n- A phone notification shows the viewer on its own and closes back to whatever was on the glasses; it no longer opens the dashboard first, and the ignore-while-active hold is honoured everywhere on custom firmware.\n- Double tap on any page returns to the main content; head-down closes only the dashboard.\n- A debug menu on the dashboard (Debug Settings toggle) queries phone-side state such as isPhoneActive().\n- The metric toggle moved to Appearance, and miniapps can read the host's preferences.\n- Messenger group threads show on the notification title row."),
8
17
  ReleaseChangelog(version = "0.1.0", markdown = "First coordinated Glassly release: the app, Glassly Engine, and the Bluetooth SDK now ship one shared release version.\n\n- The Glassly App, Glassly Engine, and the Bluetooth SDK are versioned and published together.\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."),
9
18
  )
@@ -2,10 +2,10 @@ package com.glassly.bluetoothsdk
2
2
 
3
3
  /** Generated by release CI. Do not edit in a release checkout. */
4
4
  internal object GeneratedReleaseMetadata {
5
- const val FAMILY_BASE_VERSION: String = "0.1.0"
6
- const val RELEASE_IDENTITY: String = "0.1.0-dev.98"
7
- const val RELEASE_SET_ID: String = "glassly-0.1.0-dev.98"
8
- const val SOURCE_COMMIT: String = "2c7feb7c84f8161087b58ef18cf589254ad16ae8"
5
+ const val FAMILY_BASE_VERSION: String = "0.2.8"
6
+ const val RELEASE_IDENTITY: String = "0.2.8-dev.444"
7
+ const val RELEASE_SET_ID: String = "glassly-0.2.8-dev.444"
8
+ const val SOURCE_COMMIT: String = "9ad99e635c0320d07373d42bb7422daf5289d49d"
9
9
  const val OTA_MANIFEST_URL: String = ""
10
10
  const val OTA_MANIFEST_SHA256: String = ""
11
11
  }