@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
@@ -0,0 +1,58 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+ /**
4
+ * Stable positive firmware message id per phone notification id. The firmware
5
+ * keys its history by this id, so the same phone notification must map to the
6
+ * same value for as long as it can still be updated.
7
+ */
8
+ class NotificationMessageIds(private val capacity: Int = DEFAULT_CAPACITY) {
9
+ // accessOrder = true makes this an LRU; eldest is evicted once over capacity.
10
+ private val assigned =
11
+ object : LinkedHashMap<String, Int>(16, 0.75f, true) {
12
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Int>): Boolean {
13
+ if (size <= this@NotificationMessageIds.capacity) return false
14
+ inUse.remove(eldest.value)
15
+ return true
16
+ }
17
+ }
18
+ private val inUse = mutableSetOf<Int>()
19
+ private var counter = FIRST_ID
20
+
21
+ init {
22
+ require(capacity > 0) { "capacity must be positive" }
23
+ }
24
+
25
+ @Synchronized
26
+ fun idFor(notificationId: String): Int {
27
+ assigned[notificationId]?.let { return it }
28
+ val id = nextFree()
29
+ inUse.add(id)
30
+ assigned[notificationId] = id
31
+ return id
32
+ }
33
+
34
+ @Synchronized
35
+ fun clear() {
36
+ assigned.clear()
37
+ inUse.clear()
38
+ counter = FIRST_ID
39
+ }
40
+
41
+ /** Wraps below the firmware's int range; skips values still mapped. */
42
+ private fun nextFree(): Int {
43
+ var attempts = 0
44
+ while (attempts <= inUse.size + 1) {
45
+ val id = counter
46
+ counter = if (counter >= MAX_ID) FIRST_ID else counter + 1
47
+ if (id !in inUse) return id
48
+ attempts++
49
+ }
50
+ return counter
51
+ }
52
+
53
+ companion object {
54
+ private const val DEFAULT_CAPACITY = 256
55
+ private const val FIRST_ID = 1000
56
+ private const val MAX_ID = 2_000_000_000
57
+ }
58
+ }
@@ -3,6 +3,9 @@ package com.glassly.bluetoothsdk.sgcs
3
3
  import com.glassly.bluetoothsdk.BluetoothSdkDefaults
4
4
  import com.glassly.bluetoothsdk.Bridge
5
5
  import com.glassly.bluetoothsdk.DeviceStore
6
+ import com.glassly.bluetoothsdk.GlassesNotificationConfig
7
+ import com.glassly.bluetoothsdk.GlassesNotificationStatus
8
+ import com.glassly.bluetoothsdk.PhoneNotificationUpload
6
9
  import com.glassly.bluetoothsdk.PhotoRequest
7
10
  import com.glassly.bluetoothsdk.utils.ConnTypes
8
11
 
@@ -13,7 +16,7 @@ import com.glassly.bluetoothsdk.utils.ConnTypes
13
16
  */
14
17
  data class SceneElement(
15
18
  val id: String,
16
- val type: String, // "text" | "rect" | "image"
19
+ val type: String, // "text" | "rect" | "image" | "circle" | "arc" | "pie" | "line" | "triangle" | "quad" | "bezier"
17
20
  val x: Int,
18
21
  val y: Int,
19
22
  val w: Int,
@@ -27,7 +30,30 @@ data class SceneElement(
27
30
  /** "mono" is fixed-pitch at [fontSize] px; other values use the default face. */
28
31
  val font: String = "default",
29
32
  val fontSize: Int = 0,
30
- )
33
+ /** Shapes: solid fill instead of an outline. */
34
+ val fill: Boolean = false,
35
+ /** Gray level 0..15, or -1 for the default (brightest). */
36
+ val color: Int = -1,
37
+ /** Point shapes: x0, y0, x1, y1, … in canvas pixels. */
38
+ val points: List<Int> = emptyList(),
39
+ val startDeg: Int = 0,
40
+ val endDeg: Int = 0,
41
+ /** Transition duration in ms (0 = none) and easing name. */
42
+ val transitionMs: Int = 0,
43
+ val easing: String = "ease",
44
+ val path: List<Int> = emptyList(),
45
+ val pathScale: Int = 256,
46
+ val fillRule: Int = 0,
47
+ val rotationQ8: Int = 0,
48
+ val pivotX: Int? = null,
49
+ val pivotY: Int? = null,
50
+ /** Stereo disparity in canvas px: x+depth on the left lens, x-depth on the right (positive floats in front). */
51
+ val depth: Int = 0,
52
+ ) {
53
+ /** The same element placed at the canvas origin (for standalone rasterization). */
54
+
55
+ fun atOrigin(): SceneElement = copy(x = 0, y = 0)
56
+ }
31
57
 
32
58
  /**
33
59
  * A whole scene from the host pipeline — the full frame plus per-element change
@@ -42,11 +68,31 @@ data class SceneFrame(
42
68
  val removed: List<String>,
43
69
  val canvasWidth: Int = DEFAULT_CANVAS_WIDTH,
44
70
  val canvasHeight: Int = DEFAULT_CANVAS_HEIGHT,
71
+ /**
72
+ * Host frame number, reported back in `scene_frame_presented` once the
73
+ * device has the frame. 0 = native-originated (legacy sugar), never reported.
74
+ */
75
+ val seq: Int = 0,
45
76
  ) {
46
77
  companion object {
47
78
  const val DEFAULT_CANVAS_WIDTH = 576
48
79
  const val DEFAULT_CANVAS_HEIGHT = 288
49
80
  }
81
+
82
+ /** What a queued transfer must report once it lands; null for native-originated frames. */
83
+ val presentTag: PresentTag?
84
+ get() = if (seq > 0) PresentTag(seq, appId) else null
85
+
86
+ /** Tell the host this frame's content is on the glasses (or why it isn't). */
87
+ fun reportPresented(presented: Boolean, reason: String? = null) {
88
+ presentTag?.report(presented, reason)
89
+ }
90
+
91
+ data class PresentTag(val seq: Int, val appId: String) {
92
+ fun report(presented: Boolean, reason: String? = null) {
93
+ Bridge.sendSceneFramePresented(seq, appId, presented, reason)
94
+ }
95
+ }
50
96
  }
51
97
 
52
98
  abstract class SGCManager {
@@ -230,6 +276,8 @@ abstract class SGCManager {
230
276
  for (id in frame.removed) {
231
277
  if (id !in paintedIds) removeLayoutElement(id, frame.appId)
232
278
  }
279
+ // No device ack on this path: the verbs above are the best signal there is.
280
+ frame.reportPresented(true)
233
281
  }
234
282
 
235
283
  /**
@@ -268,6 +316,19 @@ abstract class SGCManager {
268
316
  */
269
317
  open suspend fun cfwSceneRendererToggled(enabled: Boolean) {}
270
318
 
319
+ /**
320
+ * Live cfw_fast_ble switch (G2 Glassly CFW revision 26+): ask the lenses
321
+ * for the 7.5 ms fast connection profile, or return them to stock.
322
+ */
323
+ open suspend fun cfwFastBleToggled(enabled: Boolean) {}
324
+
325
+ /**
326
+ * True when the driver renders scenes itself with link-driven last-wins
327
+ * coalescing; DeviceManager then skips the display pacer.
328
+ */
329
+ open val supportsCfwSceneRendering: Boolean
330
+ get() = false
331
+
271
332
  // Dashboard Menu (default no-op — only G2 supports this)
272
333
  open fun setDashboardMenu(items: List<Map<String, Any>>) {}
273
334
 
@@ -280,6 +341,17 @@ abstract class SGCManager {
280
341
  // Notification Panel (default no-op — only G2 supports this)
281
342
  open suspend fun showNotificationsPanel() {}
282
343
 
344
+ // Driver-neutral glasses notification surface. Default: unsupported.
345
+ open fun setGlassesNotificationConfig(config: GlassesNotificationConfig) {
346
+ throw UnsupportedOperationException("Glasses notifications are not supported")
347
+ }
348
+
349
+ open fun glassesNotificationStatus(): GlassesNotificationStatus = GlassesNotificationStatus.unsupported()
350
+
351
+ open fun uploadPhoneNotification(upload: PhoneNotificationUpload) {
352
+ throw UnsupportedOperationException("Phone notification upload is not supported")
353
+ }
354
+
283
355
  // Controller bridging (default no-op — only G2 supports pairing with a ring controller)
284
356
  open fun connectController() {}
285
357
  open fun disconnectController() {}
@@ -339,6 +411,9 @@ abstract class SGCManager {
339
411
  abstract fun ping()
340
412
  abstract fun dbg1()
341
413
  abstract fun dbg2()
414
+ // No-op debug hooks; only G2 wires them.
415
+ open fun dbg3() {}
416
+ open fun dbg4() {}
342
417
 
343
418
  // Network Management
344
419
  abstract fun requestWifiScan(scanId: String?)
@@ -48,7 +48,7 @@ object SpeakerIdEngine {
48
48
  private const val ENROLL_CHUNK_COUNT = 3
49
49
 
50
50
  private const val SAMPLE_RATE = 16000
51
- private const val PREFS_NAME = "GlasslyPrefs"
51
+ private const val PREFS_NAME = "BluetoothSdkPrefs"
52
52
  private const val KEY_MODEL_PATH = "SpeakerIdModelPath"
53
53
 
54
54
  data class Profile(val id: String, val name: String, val embedding: FloatArray)
@@ -59,6 +59,9 @@ internal data class GlassesStatus(
59
59
  val wifi: WifiStatus,
60
60
  val batteryLevel: Int,
61
61
  val charging: Boolean,
62
+ val ringConnected: Boolean,
63
+ val ringBatteryLevel: Int,
64
+ val ringCharging: Boolean,
62
65
  val caseBatteryLevel: Int,
63
66
  val caseCharging: Boolean,
64
67
  val caseOpen: Boolean,
@@ -71,6 +74,13 @@ internal data class GlassesStatus(
71
74
  val controllerBatteryLevel: Int,
72
75
  val controllerSignalStrength: Int,
73
76
  val ringSignalStrength: Int,
77
+ // G2 Glassly CFW keyboard bridge (revision 39+).
78
+ val keyboardBridge: Boolean = false,
79
+ val keyboardConnected: Boolean = false,
80
+ val keyboardName: String = "",
81
+ val keyboardSaved: Boolean = false,
82
+ val keyboardState: String = "idle",
83
+ val keyboardError: String = "",
74
84
  ) {
75
85
  internal fun toMap(): Map<String, Any> {
76
86
  val values =
@@ -102,6 +112,9 @@ internal data class GlassesStatus(
102
112
  "wifi" to wifi.toMap(),
103
113
  "batteryLevel" to batteryLevel,
104
114
  "charging" to charging,
115
+ "ringConnected" to ringConnected,
116
+ "ringBatteryLevel" to ringBatteryLevel,
117
+ "ringCharging" to ringCharging,
105
118
  "caseBatteryLevel" to caseBatteryLevel,
106
119
  "caseCharging" to caseCharging,
107
120
  "caseOpen" to caseOpen,
@@ -114,6 +127,12 @@ internal data class GlassesStatus(
114
127
  "controllerBatteryLevel" to controllerBatteryLevel,
115
128
  "controllerSignalStrength" to controllerSignalStrength,
116
129
  "ringSignalStrength" to ringSignalStrength,
130
+ "keyboardBridge" to keyboardBridge,
131
+ "keyboardConnected" to keyboardConnected,
132
+ "keyboardName" to keyboardName,
133
+ "keyboardSaved" to keyboardSaved,
134
+ "keyboardState" to keyboardState,
135
+ "keyboardError" to keyboardError,
117
136
  )
118
137
  systemTimeMs?.let { values["systemTimeMs"] = it }
119
138
  return values
@@ -154,6 +173,9 @@ internal data class GlassesStatus(
154
173
  wifi = WifiStatus.fromStoreMap(values) ?: WifiStatus.Disconnected,
155
174
  batteryLevel = numberValue(values, "batteryLevel") ?: -1,
156
175
  charging = boolValue(values, "charging") ?: false,
176
+ ringConnected = boolValue(values, "ringConnected") ?: false,
177
+ ringBatteryLevel = numberValue(values, "ringBatteryLevel") ?: -1,
178
+ ringCharging = boolValue(values, "ringCharging") ?: false,
157
179
  caseBatteryLevel = numberValue(values, "caseBatteryLevel") ?: -1,
158
180
  caseCharging = boolValue(values, "caseCharging") ?: false,
159
181
  caseOpen = boolValue(values, "caseOpen") ?: true,
@@ -166,6 +188,12 @@ internal data class GlassesStatus(
166
188
  controllerBatteryLevel = numberValue(values, "controllerBatteryLevel") ?: -1,
167
189
  controllerSignalStrength = numberValue(values, "controllerSignalStrength") ?: -1,
168
190
  ringSignalStrength = numberValue(values, "ringSignalStrength") ?: -1,
191
+ keyboardBridge = boolValue(values, "keyboardBridge") ?: false,
192
+ keyboardConnected = boolValue(values, "keyboardConnected") ?: false,
193
+ keyboardName = stringValue(values, "keyboardName") ?: "",
194
+ keyboardSaved = boolValue(values, "keyboardSaved") ?: false,
195
+ keyboardState = stringValue(values, "keyboardState") ?: "idle",
196
+ keyboardError = stringValue(values, "keyboardError") ?: "",
169
197
  )
170
198
  }
171
199
  }
@@ -399,6 +427,9 @@ internal data class GlassesStatusUpdate(
399
427
  val wifi: WifiStatus? = null,
400
428
  val batteryLevel: Int? = null,
401
429
  val charging: Boolean? = null,
430
+ val ringConnected: Boolean? = null,
431
+ val ringBatteryLevel: Int? = null,
432
+ val ringCharging: Boolean? = null,
402
433
  val caseBatteryLevel: Int? = null,
403
434
  val caseCharging: Boolean? = null,
404
435
  val caseOpen: Boolean? = null,
@@ -411,6 +442,12 @@ internal data class GlassesStatusUpdate(
411
442
  val controllerBatteryLevel: Int? = null,
412
443
  val controllerSignalStrength: Int? = null,
413
444
  val ringSignalStrength: Int? = null,
445
+ val keyboardBridge: Boolean? = null,
446
+ val keyboardConnected: Boolean? = null,
447
+ val keyboardName: String? = null,
448
+ val keyboardSaved: Boolean? = null,
449
+ val keyboardState: String? = null,
450
+ val keyboardError: String? = null,
414
451
  ) {
415
452
  internal fun toMap(): Map<String, Any> =
416
453
  buildMap {
@@ -452,6 +489,9 @@ internal data class GlassesStatusUpdate(
452
489
  }
453
490
  putIfNotNull("batteryLevel", batteryLevel)
454
491
  putIfNotNull("charging", charging)
492
+ putIfNotNull("ringConnected", ringConnected)
493
+ putIfNotNull("ringBatteryLevel", ringBatteryLevel)
494
+ putIfNotNull("ringCharging", ringCharging)
455
495
  putIfNotNull("caseBatteryLevel", caseBatteryLevel)
456
496
  putIfNotNull("caseCharging", caseCharging)
457
497
  putIfNotNull("caseOpen", caseOpen)
@@ -466,6 +506,12 @@ internal data class GlassesStatusUpdate(
466
506
  putIfNotNull("controllerBatteryLevel", controllerBatteryLevel)
467
507
  putIfNotNull("controllerSignalStrength", controllerSignalStrength)
468
508
  putIfNotNull("ringSignalStrength", ringSignalStrength)
509
+ putIfNotNull("keyboardBridge", keyboardBridge)
510
+ putIfNotNull("keyboardConnected", keyboardConnected)
511
+ putIfNotNull("keyboardName", keyboardName)
512
+ putIfNotNull("keyboardSaved", keyboardSaved)
513
+ putIfNotNull("keyboardState", keyboardState)
514
+ putIfNotNull("keyboardError", keyboardError)
469
515
  }
470
516
 
471
517
  companion object {
@@ -507,6 +553,9 @@ internal data class GlassesStatusUpdate(
507
553
  },
508
554
  batteryLevel = optionalNumberValue(values, "batteryLevel"),
509
555
  charging = optionalBoolValue(values, "charging"),
556
+ ringConnected = optionalBoolValue(values, "ringConnected"),
557
+ ringBatteryLevel = optionalNumberValue(values, "ringBatteryLevel"),
558
+ ringCharging = optionalBoolValue(values, "ringCharging"),
510
559
  caseBatteryLevel = optionalNumberValue(values, "caseBatteryLevel"),
511
560
  caseCharging = optionalBoolValue(values, "caseCharging"),
512
561
  caseOpen = optionalBoolValue(values, "caseOpen"),
@@ -526,6 +575,12 @@ internal data class GlassesStatusUpdate(
526
575
  controllerBatteryLevel = optionalNumberValue(values, "controllerBatteryLevel"),
527
576
  controllerSignalStrength = optionalNumberValue(values, "controllerSignalStrength"),
528
577
  ringSignalStrength = optionalNumberValue(values, "ringSignalStrength"),
578
+ keyboardBridge = optionalBoolValue(values, "keyboardBridge"),
579
+ keyboardConnected = optionalBoolValue(values, "keyboardConnected"),
580
+ keyboardName = optionalStringValue(values, "keyboardName"),
581
+ keyboardSaved = optionalBoolValue(values, "keyboardSaved"),
582
+ keyboardState = optionalStringValue(values, "keyboardState"),
583
+ keyboardError = optionalStringValue(values, "keyboardError"),
529
584
  )
530
585
  }
531
586
  }
@@ -70,7 +70,7 @@ private class ProgressInputStream(
70
70
  */
71
71
  object STTTools {
72
72
  private const val TAG = "STTTools"
73
- private const val PREFS_NAME = "GlasslyPrefs"
73
+ private const val PREFS_NAME = "BluetoothSdkPrefs"
74
74
  private const val KEY_STT_MODEL_PATH = "STTModelPath"
75
75
  private const val KEY_STT_MODEL_LANGUAGE = "STTModelLanguageCode"
76
76
 
@@ -28,7 +28,7 @@ class SherpaOnnxTranscriber(private val context: Context) {
28
28
  private const val TAG = "SherpaOnnxTranscriber"
29
29
  private const val SAMPLE_RATE = 16000 // Sherpa-ONNX model's required sample rate
30
30
  private const val QUEUE_CAPACITY = 100 // Max number of audio buffers to keep in queue
31
- private const val PREFS_NAME = "GlasslyPrefs"
31
+ private const val PREFS_NAME = "BluetoothSdkPrefs"
32
32
  private const val KEY_STT_MODEL_PATH = "STTModelPath"
33
33
  private const val KEY_STT_MODEL_LANGUAGE = "STTModelLanguageCode"
34
34
  }
@@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit
22
22
  * M3, M4, M5 (sids 0..9). We default to F1 (sid 0) for a female voice.
23
23
  */
24
24
  object TTSTools {
25
- private const val PREFS_NAME = "GlasslyPrefs"
25
+ private const val PREFS_NAME = "BluetoothSdkPrefs"
26
26
  private const val KEY_TTS_MODEL_PATH = "TTSModelPath"
27
27
  private const val KEY_TTS_MODEL_LANGUAGE = "TTSModelLanguageCode"
28
28
 
@@ -10,25 +10,26 @@ import android.util.Log
10
10
  import com.glassly.bluetoothsdk.Bridge
11
11
  import com.glassly.bluetoothsdk.DeviceStore
12
12
 
13
- import glassly.ble.GlasslyBle.DisplayText
14
- import glassly.ble.GlasslyBle.ClearDisplay
15
- import glassly.ble.GlasslyBle.DisconnectRequest
16
- import glassly.ble.GlasslyBle.PhoneToGlasses
17
- import glassly.ble.GlasslyBle.DisplayImage
18
- import glassly.ble.GlasslyBle.CanvasCreateComponent
19
- import glassly.ble.GlasslyBle.CanvasUpdateImage
20
- import glassly.ble.GlasslyBle.CanvasUpdateText
21
- import glassly.ble.GlasslyBle.CanvasDeleteComponent
22
- import glassly.ble.GlasslyBle.CanvasClear
23
- import glassly.ble.GlasslyBle.CanvasComponentType
24
- import glassly.ble.GlasslyBle.BatteryStateRequest
25
- import glassly.ble.GlasslyBle.MicStateConfig
26
- import glassly.ble.GlasslyBle.BrightnessConfig
27
- import glassly.ble.GlasslyBle.AutoBrightnessConfig
28
- import glassly.ble.GlasslyBle.HeadUpAngleConfig
29
- import glassly.ble.GlasslyBle.DisplayDistanceConfig
30
- import glassly.ble.GlasslyBle.DisplayHeightConfig
31
- import glassly.ble.GlasslyBle.VadEnabledConfig
13
+ import com.glassly.bluetoothsdk.sgcs.NexProto
14
+ import com.glassly.bluetoothsdk.sgcs.NexProto.DisplayText
15
+ import com.glassly.bluetoothsdk.sgcs.NexProto.ClearDisplay
16
+ import com.glassly.bluetoothsdk.sgcs.NexProto.DisconnectRequest
17
+ import com.glassly.bluetoothsdk.sgcs.NexProto.PhoneToGlasses
18
+ import com.glassly.bluetoothsdk.sgcs.NexProto.DisplayImage
19
+ import com.glassly.bluetoothsdk.sgcs.NexProto.CanvasCreateComponent
20
+ import com.glassly.bluetoothsdk.sgcs.NexProto.CanvasUpdateImage
21
+ import com.glassly.bluetoothsdk.sgcs.NexProto.CanvasUpdateText
22
+ import com.glassly.bluetoothsdk.sgcs.NexProto.CanvasDeleteComponent
23
+ import com.glassly.bluetoothsdk.sgcs.NexProto.CanvasClear
24
+ import com.glassly.bluetoothsdk.sgcs.NexProto.CanvasComponentType
25
+ import com.glassly.bluetoothsdk.sgcs.NexProto.BatteryStateRequest
26
+ import com.glassly.bluetoothsdk.sgcs.NexProto.MicStateConfig
27
+ import com.glassly.bluetoothsdk.sgcs.NexProto.BrightnessConfig
28
+ import com.glassly.bluetoothsdk.sgcs.NexProto.AutoBrightnessConfig
29
+ import com.glassly.bluetoothsdk.sgcs.NexProto.HeadUpAngleConfig
30
+ import com.glassly.bluetoothsdk.sgcs.NexProto.DisplayDistanceConfig
31
+ import com.glassly.bluetoothsdk.sgcs.NexProto.DisplayHeightConfig
32
+ import com.glassly.bluetoothsdk.sgcs.NexProto.VadEnabledConfig
32
33
 
33
34
  import org.json.JSONArray
34
35
  import org.json.JSONException
@@ -138,73 +139,10 @@ object NexProtobufUtils {
138
139
  return chunks
139
140
  }
140
141
 
141
- /**
142
- * Gets the current protobuf schema version from the compiled protobuf descriptor
143
- */
144
- fun getProtobufSchemaVersion(context: Context): Int {
145
- return try {
146
- // Get the protobuf descriptor and extract the schema version
147
- val fileDescriptor = glassly.ble.GlasslyBle.getDescriptor().file
148
-
149
- Log.d(TAG, "Proto file descriptor: ${fileDescriptor.name}")
150
-
151
- // Method 1: Try to access the custom glassly_schema_version option
152
- try {
153
- // Get the file options from the descriptor
154
- val options = fileDescriptor.options
155
- Log.d(TAG, "Got file options: $options")
156
-
157
- // Try to access the custom option using the extension registry
158
- try {
159
- // Look for the generated extension in the GlasslyBle class
160
- glassly.ble.GlasslyBle::class.java.declaredFields.forEach { field ->
161
- if (field.name.contains("schema", ignoreCase = true) ||
162
- field.name.contains("version", ignoreCase = true)) {
163
- Log.d(TAG, "Found potential version field: ${field.name}")
164
- field.isAccessible = true
165
- try {
166
- val value = field.get(null)
167
- if (value is com.google.protobuf.Extension<*, *>) {
168
- @Suppress("UNCHECKED_CAST")
169
- val ext = value as com.google.protobuf.Extension<com.google.protobuf.DescriptorProtos.FileOptions, Int>
170
- if (options.hasExtension(ext)) {
171
- val version = options.getExtension(ext)
172
- Log.d(TAG, "Found schema version via extension: $version")
173
- return version
174
- }
175
- }
176
- } catch (fieldException: Exception) {
177
- Log.d(TAG, "Field access failed for ${field.name}: ${fieldException.message}")
178
- }
179
- }
180
- }
181
- } catch (extensionException: Exception) {
182
- Log.d(TAG, "Extension search failed: ${extensionException.message}")
183
- }
184
-
185
- } catch (optionsException: Exception) {
186
- Log.d(TAG, "Options access failed: ${optionsException.message}")
187
- }
188
-
189
- // Method 2: Try to read from the actual proto file content
190
- try {
191
- val protoVersion = readProtoVersionFromProject(context)
192
- if (protoVersion != null) {
193
- Log.d(TAG, "Read proto version from project: $protoVersion")
194
- return protoVersion.toInt()
195
- }
196
- } catch (projectException: Exception) {
197
- Log.d(TAG, "Project file reading failed: ${projectException.message}")
198
- }
199
-
200
- Log.w(TAG, "Could not extract protobuf schema version dynamically, using fallback")
201
- 1 // Fallback to version 1
202
-
203
- } catch (e: Exception) {
204
- Log.e(TAG, "Error getting protobuf schema version: ${e.message}", e)
205
- 1 // Fallback to version 1
206
- }
207
- }
142
+ /** Schema version of proto/nex_ble.proto. The schema carries no version option; bump alongside the proto. */
143
+ const val PROTOBUF_SCHEMA_VERSION = 1
144
+
145
+ fun getProtobufSchemaVersion(): Int = PROTOBUF_SCHEMA_VERSION
208
146
 
209
147
  fun generateClearDisplayRequestCommandBytes(): ByteArray {
210
148
  // Create the clear display protobuf message
@@ -576,69 +514,6 @@ object NexProtobufUtils {
576
514
 
577
515
  Bridge.log("Nex: $logMessage")
578
516
  }
579
-
580
- private fun readProtoVersionFromAssets(context: Context): String? {
581
- return try {
582
- context.assets.open("glassly_ble.proto").use { inputStream ->
583
- val content = inputStream.bufferedReader().use { it.readText() }
584
- extractVersionFromProtoContent(content)
585
- }
586
- } catch (e: IOException) {
587
- Bridge.log("Could not read proto from assets: ${e.message}")
588
- null
589
- }
590
- }
591
-
592
- private fun readProtoVersionFromResources(context: Context): String? {
593
- return try {
594
- val resId = context.resources.getIdentifier("glassly_ble", "raw", context.packageName)
595
- if (resId != 0) {
596
- context.resources.openRawResource(resId).use { inputStream ->
597
- val content = inputStream.bufferedReader().use { it.readText() }
598
- extractVersionFromProtoContent(content)
599
- }
600
- } else {
601
- null
602
- }
603
- } catch (e: Exception) {
604
- Bridge.log("Could not read proto from resources: ${e.message}")
605
- null
606
- }
607
- }
608
-
609
- private fun readProtoVersionFromProject(context: Context): String? {
610
- val projectPaths = arrayOf(
611
- "../../mcu_client/glassly_ble.proto",
612
- "../../../mcu_client/glassly_ble.proto",
613
- "../../../../mcu_client/glassly_ble.proto",
614
- "/data/data/${context.packageName}/../../mcu_client/glassly_ble.proto",
615
- "${android.os.Environment.getExternalStorageDirectory()}/Glassly/mcu_client/glassly_ble.proto"
616
- )
617
-
618
- return projectPaths.firstNotNullOfOrNull { path ->
619
- try {
620
- val protoFile = java.io.File(path)
621
- if (protoFile.exists() && protoFile.canRead()) {
622
- val content = protoFile.readText(Charsets.UTF_8)
623
- extractVersionFromProtoContent(content)
624
- } else {
625
- null
626
- }
627
- } catch (e: Exception) {
628
- null
629
- }
630
- }
631
- }
632
-
633
- private fun extractVersionFromProtoContent(content: String): String? {
634
- return try {
635
- val pattern = Regex("""option\s*\(\s*glassly_schema_version\s*\)\s*=\s*(\d+)\s*;""")
636
- pattern.find(content)?.groupValues?.get(1)
637
- } catch (e: Exception) {
638
- Log.e(TAG, "Error extracting version from proto content: ${e.message}", e)
639
- null
640
- }
641
- }
642
517
  }
643
518
 
644
519
  object NexEventUtils {