@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.
- 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 +1065 -255
- 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 +1420 -495
- 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,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
|
|
186
|
+
private const val MANUFACTURER_ID = 0xB822
|
|
187
187
|
|
|
188
188
|
private fun manufacturerData(result: ScanResult): ByteArray? {
|
|
189
|
-
return result.scanRecord?.getManufacturerSpecificData(
|
|
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
|
|
5898
|
+
val bondedLiveDevices =
|
|
5899
5899
|
bonded.filter { isMentraLiveBluetoothName(safeDeviceName(it)) }
|
|
5900
|
-
if (
|
|
5900
|
+
if (bondedLiveDevices.isEmpty()) {
|
|
5901
5901
|
return
|
|
5902
5902
|
}
|
|
5903
5903
|
val currentAddress = connectedDevice?.address
|
|
5904
|
-
for (device in
|
|
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.
|
|
34
|
-
import glassly.
|
|
35
|
-
import glassly.
|
|
36
|
-
import glassly.
|
|
37
|
-
import glassly.
|
|
38
|
-
import glassly.
|
|
39
|
-
import glassly.
|
|
40
|
-
import glassly.
|
|
41
|
-
import glassly.
|
|
42
|
-
import glassly.
|
|
43
|
-
import glassly.
|
|
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(
|
|
1295
|
+
val schemaVersion = NexProtobufUtils.getProtobufSchemaVersion()
|
|
1296
1296
|
|
|
1297
1297
|
// Build the info string directly instead of calling getProtobufBuildInfo()
|
|
1298
|
-
val fileDescriptorName =
|
|
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 !=
|
|
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
|
}
|