@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.
- package/README.md +47 -6
- package/android/build.gradle +7 -7
- package/android/lc3Lib/build.gradle +2 -2
- package/android/src/main/assets/G2Firmware/README.md +27 -2
- 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
- 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
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
- package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1075 -257
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
- package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
- package/build/BluetoothSdk.types.d.ts +209 -20
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -1
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +30 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +28 -1
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +37 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +102 -13
- package/ios/GlasslyBluetoothSDK.podspec +1 -1
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
- package/ios/Source/BluetoothSDK.swift +53 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +13 -0
- package/ios/Source/DeviceManager.swift +151 -33
- package/ios/Source/DeviceStore.swift +28 -14
- package/ios/Source/DisplayPacer.swift +180 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
- package/ios/Source/GlassesNotifications.swift +181 -0
- package/ios/Source/ObservableStore.swift +14 -2
- package/ios/Source/Resources/G2Firmware/README.md +27 -2
- package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/controllers/R1.swift +0 -1
- package/ios/Source/internal/BluetoothAvailability.swift +31 -0
- package/ios/Source/sgcs/G2.swift +1434 -504
- package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
- package/ios/Source/sgcs/G2BleLink.swift +102 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
- package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
- package/ios/Source/sgcs/G2CfwScene.swift +779 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
- package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
- package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
- package/ios/Source/sgcs/G2Flasher.swift +9 -9
- package/ios/Source/sgcs/G2Keyboard.swift +780 -0
- package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
- package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
- package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
- package/ios/Source/sgcs/G2RingBattery.swift +22 -0
- package/ios/Source/sgcs/G2Sensors.swift +75 -0
- package/ios/Source/sgcs/G2TouchInput.swift +51 -0
- package/ios/Source/sgcs/G2WritePacing.swift +18 -0
- package/ios/Source/sgcs/MentraNex.swift +44 -44
- package/ios/Source/sgcs/SGCManager.swift +120 -13
- package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
- package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
- package/ios/Source/status/DeviceStatus.swift +27 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
- package/ios/Source/utils/Enums.swift +0 -1
- package/ios/Source/utils/G1Text.swift +0 -1
- package/ios/Source/utils/G2NotifyFile.swift +20 -10
- package/ios/Source/utils/JSCExperiment.swift +6 -6
- package/ios/Source/utils/Models.swift +0 -1
- package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
- package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
- package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
- package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
- package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
- package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
- package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
- package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
- package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
- package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
- package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
- package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
- package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +2 -2
- package/plugin/build/withIos.js +1 -1
- package/scripts/gen-nex-proto.sh +12 -0
- package/scripts/inject-ios-sdk-version.mjs +1 -1
- package/src/BluetoothSdk.types.ts +214 -19
- package/src/_internal.ts +1 -1
- package/src/_private/BluetoothSdkModule.ts +40 -0
- package/src/generated/changelogCatalog.ts +37 -1
- package/src/generated/releaseMetadata.ts +4 -4
- 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 = "
|
|
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 = "
|
|
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 = "
|
|
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 = "
|
|
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.
|
|
14
|
-
import glassly.
|
|
15
|
-
import glassly.
|
|
16
|
-
import glassly.
|
|
17
|
-
import glassly.
|
|
18
|
-
import glassly.
|
|
19
|
-
import glassly.
|
|
20
|
-
import glassly.
|
|
21
|
-
import glassly.
|
|
22
|
-
import glassly.
|
|
23
|
-
import glassly.
|
|
24
|
-
import glassly.
|
|
25
|
-
import glassly.
|
|
26
|
-
import glassly.
|
|
27
|
-
import glassly.
|
|
28
|
-
import glassly.
|
|
29
|
-
import glassly.
|
|
30
|
-
import glassly.
|
|
31
|
-
import glassly.
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
fun getProtobufSchemaVersion(
|
|
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 {
|