@glassly/bluetooth-sdk 0.1.0-dev.99 → 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 +1065 -255
  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 +1420 -495
  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,181 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+ import com.glassly.bluetoothsdk.AllowedApp
4
+ import com.glassly.bluetoothsdk.PhoneNotificationUpload
5
+ import java.io.ByteArrayOutputStream
6
+ import java.text.SimpleDateFormat
7
+ import java.util.Date
8
+ import java.util.Locale
9
+
10
+ /**
11
+ * Wire format for the G2 file-transfer pushes (0xC4/0xC5 services). Pure so the
12
+ * header layout, CRC variant and JSON shape stay unit-testable.
13
+ */
14
+ object G2NotificationFile {
15
+ /** The firmware dispatches on the header's file type, not the filename or JSON. */
16
+ const val FILENAME = "user/notify_whitelist.json"
17
+
18
+ /**
19
+ * eEvenFileServiceType from the stock app's efs_transmit.proto. A file typed
20
+ * as a notification whose JSON has no `android_notification` node leaves a
21
+ * blank card in the stock viewer.
22
+ */
23
+ const val FILE_TYPE_WHITELIST = 0 // NOTIFICATION_JSON_WHITELIST
24
+ const val FILE_TYPE_NOTIFICATION = 1 // ANDROID_MSG_JSON_NOTIFICATION
25
+
26
+ const val HEADER_SIZE = 93
27
+ private const val FILENAME_FIELD_SIZE = 80
28
+
29
+ /** Firmware caps: 100-app list, 64-byte id and 16-byte name records incl. NUL. */
30
+ const val MAX_APPS = 100
31
+ const val MAX_IDENTIFIER_BYTES = 63
32
+ const val MAX_DISPLAY_NAME_BYTES = 15
33
+
34
+ const val MAX_TITLE_BYTES = 120
35
+ const val MAX_SUBTITLE_BYTES = 120
36
+ const val MAX_MESSAGE_BYTES = 512
37
+
38
+ /** Notification action id the firmware reads as "show this now". */
39
+ private const val ACTION_SHOW = 0
40
+
41
+ // CRC-32C (Castagnoli), init 0, non-reflected (MSB-first) — the FILE_CHECK
42
+ // variant, NOT the common reflected crc32c.
43
+ private const val POLYNOMIAL = 0x1EDC6F41L
44
+
45
+ private val crcTable: IntArray =
46
+ IntArray(256) { index ->
47
+ var crc = index.toLong() shl 24
48
+ repeat(8) {
49
+ crc = if (crc and 0x80000000L != 0L) ((crc shl 1) xor POLYNOMIAL) else (crc shl 1)
50
+ crc = crc and 0xFFFFFFFFL
51
+ }
52
+ crc.toInt()
53
+ }
54
+
55
+ fun crc32c(bytes: ByteArray): Int {
56
+ var crc = 0
57
+ for (byte in bytes) {
58
+ val index = ((crc ushr 24) xor (byte.toInt() and 0xFF)) and 0xFF
59
+ crc = (crc shl 8) xor crcTable[index]
60
+ }
61
+ return crc
62
+ }
63
+
64
+ /**
65
+ * 93-byte FILE_CHECK payload as the firmware reads it: SEND_START command
66
+ * byte, then u32 file type, u32 size, u32 CRC-32C and the NUL-padded
67
+ * filename. (The even-g2-protocol docs describe the same bytes shifted by
68
+ * one — "mode 0x100, size * 256, crc shl 8, extra" — which hides that
69
+ * "mode 0x100" is file type 1, the Android notification.)
70
+ */
71
+ fun fileCheckHeader(filename: String, content: ByteArray, fileType: Int): ByteArray {
72
+ val crc = crc32c(content)
73
+ val out = ByteArrayOutputStream(HEADER_SIZE)
74
+ out.write(0x00) // eEvenFileSendServiceCID SEND_START
75
+ writeLittleEndian(out, fileType)
76
+ writeLittleEndian(out, content.size)
77
+ writeLittleEndian(out, crc)
78
+ var name = filename.toByteArray(Charsets.UTF_8)
79
+ if (name.size > FILENAME_FIELD_SIZE) {
80
+ name = name.copyOfRange(0, FILENAME_FIELD_SIZE)
81
+ }
82
+ out.write(name)
83
+ out.write(ByteArray(FILENAME_FIELD_SIZE - name.size))
84
+ return out.toByteArray()
85
+ }
86
+
87
+ /**
88
+ * Allowlist file in the stock app's shape. Ids over
89
+ * [MAX_IDENTIFIER_BYTES] are invalid to the firmware and dropped; names
90
+ * truncate. Key order is fixed — the glasses cache by checksum.
91
+ */
92
+ fun whitelistJson(filterEnabled: Boolean, apps: List<AllowedApp>): ByteArray {
93
+ val entries = StringBuilder()
94
+ var count = 0
95
+ for (app in apps) {
96
+ if (count >= MAX_APPS) break
97
+ if (app.id.isEmpty()) continue
98
+ if (app.id.toByteArray(Charsets.UTF_8).size > MAX_IDENTIFIER_BYTES) continue
99
+ val name = app.name.ifEmpty { app.id }
100
+ if (count > 0) entries.append(',')
101
+ entries.append("{\"id\":").append(quote(app.id))
102
+ entries.append(",\"name\":").append(quote(truncateUtf8(name, MAX_DISPLAY_NAME_BYTES))).append('}')
103
+ count++
104
+ }
105
+ val json = StringBuilder()
106
+ json.append("{\"app\":{\"enable\":").append(filterEnabled)
107
+ json.append(",\"list\":[").append(entries).append("]}")
108
+ json.append(",\"calendar_enable\":true")
109
+ json.append(",\"call_enable\":true")
110
+ json.append(",\"ios_mail_enable\":true")
111
+ json.append(",\"msg_enable\":true}")
112
+ return json.toString().toByteArray(Charsets.UTF_8)
113
+ }
114
+
115
+ /**
116
+ * Notification content for the firmware to present and keep in history.
117
+ * `date` is the phone's local wall clock; `time_s` is the epoch second.
118
+ */
119
+ fun notificationJson(upload: PhoneNotificationUpload, msgId: Int, nowMs: Long): ByteArray {
120
+ val timestampMs = if (upload.timestampMs > 0) upload.timestampMs else nowMs
121
+ val formatter = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US)
122
+ val json = StringBuilder()
123
+ json.append("{\"android_notification\":{")
124
+ json.append("\"action\":").append(ACTION_SHOW)
125
+ json.append(",\"app_identifier\":").append(quote(upload.packageName.trim()))
126
+ json.append(",\"date\":").append(quote(formatter.format(Date(timestampMs))))
127
+ json.append(",\"display_name\":")
128
+ .append(quote(truncateUtf8(upload.appName.trim().ifEmpty { upload.packageName.trim() }, MAX_TITLE_BYTES)))
129
+ json.append(",\"message\":").append(quote(truncateUtf8(upload.body.trim(), MAX_MESSAGE_BYTES)))
130
+ json.append(",\"msg_id\":").append(msgId)
131
+ json.append(",\"subtitle\":").append(quote(truncateUtf8(upload.subtitle.trim(), MAX_SUBTITLE_BYTES)))
132
+ json.append(",\"time_s\":").append(timestampMs / 1000L)
133
+ json.append(",\"title\":").append(quote(truncateUtf8(upload.title.trim(), MAX_TITLE_BYTES)))
134
+ json.append("}}")
135
+ return json.toString().toByteArray(Charsets.UTF_8)
136
+ }
137
+
138
+ /** Trim to a UTF-8 byte budget without splitting a code point. */
139
+ fun truncateUtf8(value: String, maxBytes: Int): String {
140
+ if (value.toByteArray(Charsets.UTF_8).size <= maxBytes) return value
141
+ val out = StringBuilder()
142
+ var used = 0
143
+ var index = 0
144
+ while (index < value.length) {
145
+ val codePoint = value.codePointAt(index)
146
+ val charCount = Character.charCount(codePoint)
147
+ val chunk = value.substring(index, index + charCount)
148
+ val size = chunk.toByteArray(Charsets.UTF_8).size
149
+ if (used + size > maxBytes) break
150
+ out.append(chunk)
151
+ used += size
152
+ index += charCount
153
+ }
154
+ return out.toString()
155
+ }
156
+
157
+ private fun writeLittleEndian(out: ByteArrayOutputStream, value: Int) {
158
+ out.write(value and 0xFF)
159
+ out.write((value ushr 8) and 0xFF)
160
+ out.write((value ushr 16) and 0xFF)
161
+ out.write((value ushr 24) and 0xFF)
162
+ }
163
+
164
+ private fun quote(value: String): String {
165
+ val out = StringBuilder(value.length + 2)
166
+ out.append('"')
167
+ for (char in value) {
168
+ when {
169
+ char == '"' -> out.append("\\\"")
170
+ char == '\\' -> out.append("\\\\")
171
+ char == '\n' -> out.append("\\n")
172
+ char == '\r' -> out.append("\\r")
173
+ char == '\t' -> out.append("\\t")
174
+ char < ' ' -> out.append(String.format(Locale.US, "\\u%04x", char.code))
175
+ else -> out.append(char)
176
+ }
177
+ }
178
+ out.append('"')
179
+ return out.toString()
180
+ }
181
+ }
@@ -0,0 +1,20 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+ /** Settings field 106, sent as its own notification after the device-info poll by GLASSLYCFW/24 and later.
4
+ * This is the R1 connected to the glasses, independent of the phone controller.
5
+ */
6
+ internal data class G2RingBattery(val connected: Boolean, val level: Int, val charging: Boolean) {
7
+ companion object {
8
+ val DISCONNECTED = G2RingBattery(false, -1, false)
9
+
10
+ fun decode(data: ByteArray): G2RingBattery? {
11
+ if (data.size != 5 || data[0] != 0x52.toByte() || data[1] != 0x42.toByte() || data[2] != 1.toByte()) return null
12
+ val flags = data[3].toInt() and 0xff
13
+ if (flags and 1 == 0) return DISCONNECTED
14
+ if (flags and 2 == 0) return G2RingBattery(true, -1, false)
15
+ val level = data[4].toInt() and 0xff
16
+ if (level > 100) return null
17
+ return G2RingBattery(true, level, flags and 4 != 0)
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,67 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+ /** Upstream image modes 16/10 and settings/navigation extension reports. */
4
+ internal object G2Sensors {
5
+ fun heading(fields: Map<Int, Any>): Int? {
6
+ if (!fields.containsKey(1)) return 0
7
+ return (fields[1] as? Int)?.takeIf { it in 0..359 }
8
+ }
9
+
10
+ fun command(sensor: String, operation: String, options: Map<String, Double>): ByteArray? {
11
+ fun number(key: String, fallback: Int, range: IntRange): Int? {
12
+ val value = options[key] ?: return fallback
13
+ if (!value.isFinite() || value % 1.0 != 0.0 || value < range.first || value > range.last) return null
14
+ return value.toInt()
15
+ }
16
+ fun u16(n: Int) = byteArrayOf(n.toByte(), (n shr 8).toByte())
17
+ if (sensor == "light") {
18
+ if (operation == "query") return byteArrayOf(16, 0)
19
+ if (operation == "stop") return byteArrayOf(16, 2)
20
+ if (operation != "start") return null
21
+ val interval = number("intervalMs", 500, 100..5000) ?: return null
22
+ val delta = number("minDelta", 1, 0..65535) ?: return null
23
+ val heartbeat = number("heartbeatMs", 5000, 0..65535) ?: return null
24
+ return byteArrayOf(16, 1, 1) + u16(interval) + u16(delta) + u16(heartbeat)
25
+ }
26
+ if (sensor == "compass") {
27
+ if (operation == "stop") return byteArrayOf(10, 0)
28
+ if (operation != "start") return null
29
+ val interval = number("intervalMs", 1000, 50..2000) ?: return null
30
+ val change = number("minChangeDegrees", 5, 0..359) ?: return null
31
+ return byteArrayOf(10, 2) + u16(interval) + u16(change)
32
+ }
33
+ return null
34
+ }
35
+ private fun uint(b: ByteArray, offset: Int, count: Int): Long =
36
+ (0 until count).fold(0L) { n, i -> n or ((b[offset + i].toLong() and 255) shl (8 * i)) }
37
+ fun light(b: ByteArray): Map<String, Any>? {
38
+ if (b.size != 24 || b[0] != 65.toByte() || b[1] != 76.toByte() || b[2] != 1.toByte()) return null
39
+ val reason = b[3].toInt() and 255
40
+ if (reason > 3) return null
41
+ val flags = b[4].toInt()
42
+ return mapOf(
43
+ "reason" to listOf("query", "started", "poll", "stopped")[reason],
44
+ "opened" to (flags and 1 != 0), "passive" to (flags and 2 != 0),
45
+ "autoBrightness" to (flags and 4 != 0), "readSucceeded" to (flags and 8 != 0),
46
+ "status" to (b[5].toInt() and 255), "value" to uint(b, 6, 4), "peak" to uint(b, 10, 4),
47
+ "targetLevel" to uint(b, 14, 2), "brightness" to (b[16].toInt() and 255),
48
+ "gear" to (b[17].toInt() and 255), "scaleQ10" to uint(b, 18, 2),
49
+ "sampleTimestampMs" to uint(b, 20, 4),
50
+ )
51
+ }
52
+ fun compass(b: ByteArray): Map<String, Any?>? {
53
+ if (b.size != 12 || b[0] != 67.toByte() || b[1] != 77.toByte() || b[2] != 1.toByte()) return null
54
+ val accuracy = b[3].toInt() and 255
55
+ val anomalies = b[4].toInt() and 255
56
+ val source = b[5].toInt() and 255
57
+ if ((accuracy > 3 && accuracy != 255) || (anomalies > 2 && anomalies != 255) || source > 3) return null
58
+ val flags = b[6].toInt()
59
+ return mapOf(
60
+ "accuracy" to accuracy.takeUnless { it == 255 }, "anomalies" to anomalies.takeUnless { it == 255 },
61
+ "source" to listOf("unknown", "grv", "gmrv", "rv")[source],
62
+ "sampleMatched" to (flags and 128 != 0), "grvValid" to (flags and 1 != 0),
63
+ "gmrvValid" to (flags and 2 != 0), "rvValid" to (flags and 4 != 0),
64
+ "magneticDataFresh" to (flags and 8 != 0), "sampleTimestampMs" to uint(b, 8, 4),
65
+ )
66
+ }
67
+ }
@@ -0,0 +1,39 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+ /** Captured on the BLE callback thread, before posting input to the main queue. */
4
+ internal data class G2InputArrival(val elapsedMs: Long, val timestamp: Long)
5
+
6
+ /** CFW field 100: a raw R1 report forwarded once through the right lens. */
7
+ internal data class G2RingInput(val wireType: Int, val tick: Long) {
8
+ companion object {
9
+ fun parse(fields: Map<Int, Any>): G2RingInput? {
10
+ if (fields[2] != 2) return null
11
+ val data = fields[100] as? ByteArray ?: return null
12
+ if (data.size != 12) return null
13
+ val bytes = data.map { it.toInt() and 0xFF }
14
+ if (bytes[0] != 0x52 || bytes[1] != 0x49 || bytes[2] != 1 || bytes[3] != 1 || bytes[7] != 0) return null
15
+ val event = when (bytes[4]) {
16
+ 0 -> 9; 1 -> 0; 2 -> 3; 4 -> 1; 5 -> 2; 8 -> 10; 9 -> 11; 10 -> 14
17
+ else -> 127
18
+ }
19
+ if (fields[1] != event) return null
20
+ val tick = bytes[8].toLong() or (bytes[9].toLong() shl 8) or
21
+ (bytes[10].toLong() shl 16) or (bytes[11].toLong() shl 24)
22
+ return G2RingInput(bytes[4], tick)
23
+ }
24
+ }
25
+ }
26
+
27
+ internal class G2TouchDebounce {
28
+ private var lastAcceptedAt: Long? = null
29
+
30
+ fun accepts(event: Int?, ring: G2RingInput?, receivedAt: Long): Boolean {
31
+ // Preserve the raw ring stream instead of reinstating stock suppression.
32
+ // Edges and unknown markers never consume a legacy debounce slot.
33
+ if (ring != null || event == 9 || event == 10 || event == 14 || event == 127) return true
34
+ val last = lastAcceptedAt
35
+ if (last != null && receivedAt - last < 100) return false
36
+ lastAcceptedAt = receivedAt
37
+ return true
38
+ }
39
+ }
@@ -183,10 +183,10 @@ class MentraLive : SGCManager() {
183
183
  private const val POST_SHUTDOWN_RECONNECT_DELAY_MS =
184
184
  10_000L // 10s before first scan after shutdown
185
185
 
186
- private const val GLASSLY_MANUFACTURER_ID = 0xB822
186
+ private const val MANUFACTURER_ID = 0xB822
187
187
 
188
188
  private fun manufacturerData(result: ScanResult): ByteArray? {
189
- return result.scanRecord?.getManufacturerSpecificData(GLASSLY_MANUFACTURER_ID)
189
+ return result.scanRecord?.getManufacturerSpecificData(MANUFACTURER_ID)
190
190
  }
191
191
 
192
192
  private fun isPairingDiscoverable(result: ScanResult): Boolean {
@@ -5895,13 +5895,13 @@ class MentraLive : SGCManager() {
5895
5895
  Bridge.log("LIVE: Classic: discovery release — cannot list bonded: " + e.message)
5896
5896
  return
5897
5897
  }
5898
- val glasslyBonded =
5898
+ val bondedLiveDevices =
5899
5899
  bonded.filter { isMentraLiveBluetoothName(safeDeviceName(it)) }
5900
- if (glasslyBonded.isEmpty()) {
5900
+ if (bondedLiveDevices.isEmpty()) {
5901
5901
  return
5902
5902
  }
5903
5903
  val currentAddress = connectedDevice?.address
5904
- for (device in glasslyBonded) {
5904
+ for (device in bondedLiveDevices) {
5905
5905
  if (currentAddress != null && device.address.equals(currentAddress, ignoreCase = true)) {
5906
5906
  continue
5907
5907
  }
@@ -30,17 +30,17 @@ import android.os.Looper
30
30
  import android.util.Log
31
31
  import android.content.Context
32
32
 
33
- import glassly.ble.GlasslyBle.GlassesToPhone
34
- import glassly.ble.GlasslyBle.PhoneToGlasses
35
- import glassly.ble.GlasslyBle.ChargingState
36
- import glassly.ble.GlasslyBle.BatteryStatus
37
- import glassly.ble.GlasslyBle.HeadGesture
38
- import glassly.ble.GlasslyBle.ButtonEvent
39
- import glassly.ble.GlasslyBle.ImuData
40
- import glassly.ble.GlasslyBle.ImageTransferComplete
41
- import glassly.ble.GlasslyBle.HeadUpAngleResponse
42
- import glassly.ble.GlasslyBle.HeadPosition
43
- import glassly.ble.GlasslyBle.DeviceInfo
33
+ import com.glassly.bluetoothsdk.sgcs.NexProto.GlassesToPhone
34
+ import com.glassly.bluetoothsdk.sgcs.NexProto.PhoneToGlasses
35
+ import com.glassly.bluetoothsdk.sgcs.NexProto.ChargingState
36
+ import com.glassly.bluetoothsdk.sgcs.NexProto.BatteryStatus
37
+ import com.glassly.bluetoothsdk.sgcs.NexProto.HeadGesture
38
+ import com.glassly.bluetoothsdk.sgcs.NexProto.ButtonEvent
39
+ import com.glassly.bluetoothsdk.sgcs.NexProto.ImuData
40
+ import com.glassly.bluetoothsdk.sgcs.NexProto.ImageTransferComplete
41
+ import com.glassly.bluetoothsdk.sgcs.NexProto.HeadUpAngleResponse
42
+ import com.glassly.bluetoothsdk.sgcs.NexProto.HeadPosition
43
+ import com.glassly.bluetoothsdk.sgcs.NexProto.DeviceInfo
44
44
 
45
45
  import com.glassly.bluetoothsdk.sgcs.SGCManager
46
46
  import com.glassly.bluetoothsdk.Bridge
@@ -1292,10 +1292,10 @@ class MentraNex : SGCManager() {
1292
1292
  private fun postProtobufSchemaVersionInfo() {
1293
1293
  try {
1294
1294
  // Call the version method only once
1295
- val schemaVersion = NexProtobufUtils.getProtobufSchemaVersion(context!!)
1295
+ val schemaVersion = NexProtobufUtils.getProtobufSchemaVersion()
1296
1296
 
1297
1297
  // Build the info string directly instead of calling getProtobufBuildInfo()
1298
- val fileDescriptorName = glassly.ble.GlasslyBle.getDescriptor().file.name
1298
+ val fileDescriptorName = NexProto.getDescriptor().file.name
1299
1299
  val buildInfo = "Schema v$schemaVersion | $fileDescriptorName"
1300
1300
 
1301
1301
  // protobufSchemaVersion = schemaVersion.toString()
@@ -1764,7 +1764,7 @@ class MentraNex : SGCManager() {
1764
1764
  // a diagnosable log line. Non-OK codes also invalidate the
1765
1765
  // registry entry so the next frame recreates the component.
1766
1766
  val canvasResult = glassesToPhone.canvasResult
1767
- if (canvasResult.code != glassly.ble.GlasslyBle.CanvasResult.ResultCode.OK) {
1767
+ if (canvasResult.code != NexProto.CanvasResult.ResultCode.OK) {
1768
1768
  Bridge.log("Nex: CANVAS_RESULT id=${canvasResult.id} code=${canvasResult.code} — dropping registry entry for recreate")
1769
1769
  canvasElements.entries.removeAll { it.value.firmwareId == canvasResult.id.toInt() }
1770
1770
  }