@mentra/bluetooth-sdk 0.1.11 → 0.1.13
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 +67 -11
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +73 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +10 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +83 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +266 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +93 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
- package/build/BluetoothSdk.types.d.ts +72 -10
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -7
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -2
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +3 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +43 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +16 -5
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +59 -64
- package/ios/Source/BluetoothSdkDefaults.swift +47 -0
- package/ios/Source/Bridge.swift +6 -0
- package/ios/Source/DeviceManager.swift +46 -32
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +357 -34
- package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
- package/ios/Source/ObservableStore.swift +11 -0
- package/ios/Source/OtaManifest.swift +170 -0
- package/ios/Source/camera/CameraModels.swift +267 -9
- package/ios/Source/controllers/ControllerManager.swift +2 -5
- package/ios/Source/controllers/R1.swift +2 -5
- package/ios/Source/events/BluetoothEvents.swift +3 -0
- package/ios/Source/internal/BluetoothAvailability.swift +20 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
- package/ios/Source/sgcs/G1.swift +6 -5
- package/ios/Source/sgcs/G2.swift +20 -18
- package/ios/Source/sgcs/Mach1.swift +6 -5
- package/ios/Source/sgcs/MentraLive.swift +158 -38
- package/ios/Source/sgcs/MentraNex.swift +6 -5
- package/ios/Source/sgcs/SGCManager.swift +16 -7
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/status/DeviceStatus.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -1
- package/package.json +6 -1
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/withAndroid.d.ts +2 -3
- package/plugin/build/withAndroid.js +46 -0
- package/plugin/build/withIos.d.ts +2 -3
- package/plugin/build/withIos.js +32 -0
- package/src/BluetoothSdk.types.ts +80 -10
- package/src/_private/BluetoothSdkModule.ts +13 -10
- package/src/_private/photoRequestPayload.ts +45 -2
- package/src/debug.ts +9 -0
- package/src/index.ts +25 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7405
|
@@ -0,0 +1,3835 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import android.bluetooth.BluetoothAdapter
|
|
5
|
+
import android.bluetooth.BluetoothDevice
|
|
6
|
+
import android.bluetooth.BluetoothGatt
|
|
7
|
+
import android.bluetooth.BluetoothGattCallback
|
|
8
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
9
|
+
import android.bluetooth.BluetoothGattDescriptor
|
|
10
|
+
import android.bluetooth.BluetoothGattService
|
|
11
|
+
import android.bluetooth.BluetoothProfile
|
|
12
|
+
import android.bluetooth.le.BluetoothLeScanner
|
|
13
|
+
import android.bluetooth.le.ScanCallback
|
|
14
|
+
import android.bluetooth.le.ScanFilter
|
|
15
|
+
import android.bluetooth.le.ScanResult
|
|
16
|
+
import android.bluetooth.le.ScanSettings
|
|
17
|
+
import android.content.BroadcastReceiver
|
|
18
|
+
import android.content.Context
|
|
19
|
+
import android.content.Intent
|
|
20
|
+
import android.content.IntentFilter
|
|
21
|
+
import android.graphics.Bitmap
|
|
22
|
+
import android.graphics.BitmapFactory
|
|
23
|
+
import android.os.Handler
|
|
24
|
+
import android.os.Looper
|
|
25
|
+
import android.util.Log
|
|
26
|
+
import android.util.SparseArray
|
|
27
|
+
|
|
28
|
+
import java.io.IOException
|
|
29
|
+
import java.io.InputStream
|
|
30
|
+
import java.util.concurrent.BlockingQueue
|
|
31
|
+
import java.util.concurrent.CountDownLatch
|
|
32
|
+
import java.util.concurrent.TimeUnit
|
|
33
|
+
import java.util.concurrent.ExecutorService
|
|
34
|
+
import java.util.concurrent.Executors
|
|
35
|
+
|
|
36
|
+
//BMP
|
|
37
|
+
import java.util.ArrayList
|
|
38
|
+
import java.util.concurrent.LinkedBlockingQueue
|
|
39
|
+
import java.util.zip.CRC32
|
|
40
|
+
import java.nio.ByteBuffer
|
|
41
|
+
|
|
42
|
+
import com.google.gson.Gson
|
|
43
|
+
|
|
44
|
+
import org.greenrobot.eventbus.EventBus
|
|
45
|
+
import org.json.JSONArray
|
|
46
|
+
import org.json.JSONException
|
|
47
|
+
import org.json.JSONObject
|
|
48
|
+
|
|
49
|
+
import java.lang.reflect.Method
|
|
50
|
+
import java.nio.charset.StandardCharsets
|
|
51
|
+
import java.util.Arrays
|
|
52
|
+
import java.util.UUID
|
|
53
|
+
import java.util.concurrent.Semaphore
|
|
54
|
+
import java.util.regex.Matcher
|
|
55
|
+
import java.util.regex.Pattern
|
|
56
|
+
import java.util.HashMap
|
|
57
|
+
import java.util.HashSet
|
|
58
|
+
|
|
59
|
+
// Mentra
|
|
60
|
+
import com.mentra.bluetoothsdk.DeviceManager
|
|
61
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
62
|
+
import com.mentra.bluetoothsdk.PhotoRequest
|
|
63
|
+
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
64
|
+
import com.mentra.bluetoothsdk.utils.BitmapJavaUtils
|
|
65
|
+
|
|
66
|
+
import com.mentra.bluetoothsdk.utils.BitmapJavaUtils.convertBitmapTo1BitBmpBytes
|
|
67
|
+
|
|
68
|
+
import com.mentra.bluetoothsdk.utils.G1Text
|
|
69
|
+
import com.mentra.bluetoothsdk.utils.SmartGlassesConnectionState
|
|
70
|
+
import com.mentra.lc3Lib.Lc3Cpp
|
|
71
|
+
import com.mentra.bluetoothsdk.DeviceStore
|
|
72
|
+
|
|
73
|
+
class G1 : SGCManager() {
|
|
74
|
+
|
|
75
|
+
companion object {
|
|
76
|
+
private const val TAG = "WearableAi_EvenRealitiesG1SGC"
|
|
77
|
+
const val SHARED_PREFS_NAME = "EvenRealitiesPrefs"
|
|
78
|
+
|
|
79
|
+
const val LEFT_DEVICE_KEY = "SavedG1LeftName"
|
|
80
|
+
const val RIGHT_DEVICE_KEY = "SavedG1RightName"
|
|
81
|
+
|
|
82
|
+
private val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
83
|
+
private val UART_TX_CHAR_UUID: UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
84
|
+
private val UART_RX_CHAR_UUID: UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
85
|
+
private val CLIENT_CHARACTERISTIC_CONFIG_UUID: UUID = UUID
|
|
86
|
+
.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
|
87
|
+
private const val SAVED_G1_ID_KEY = "SAVED_G1_ID_KEY"
|
|
88
|
+
|
|
89
|
+
private const val DELAY_BETWEEN_SENDS_MS = 5L // not using now
|
|
90
|
+
private const val DELAY_BETWEEN_CHUNKS_SEND = 5L // super small just in case
|
|
91
|
+
private const val DELAY_BETWEEN_ACTIONS_SEND = 250L // not using now
|
|
92
|
+
private const val HEARTBEAT_INTERVAL_MS = 15000L
|
|
93
|
+
private const val MICBEAT_INTERVAL_MS = (1000L * 60) * 30 // micbeat every 30 minutes
|
|
94
|
+
|
|
95
|
+
private const val BASE_RECONNECT_DELAY_MS = 3000L // Start with 3 seconds
|
|
96
|
+
private const val MAX_RECONNECT_DELAY_MS = 60000L
|
|
97
|
+
|
|
98
|
+
private const val BOND_RETRY_DELAY_MS = 5000L // 5-second backoff
|
|
99
|
+
|
|
100
|
+
private const val CONNECTION_TIMEOUT_MS = 10000L // 10 seconds
|
|
101
|
+
|
|
102
|
+
private const val DEBOUNCE_DELAY_MS = 270L // Minimum time between chunk sends
|
|
103
|
+
|
|
104
|
+
private const val RIGHT_CONNECTION_RETRY_DELAY = 1000L // 1 second
|
|
105
|
+
|
|
106
|
+
private const val INITIAL_CONNECTION_DELAY_MS = 350L // Adjust this value as needed
|
|
107
|
+
|
|
108
|
+
private const val NOTIFICATION = 0x4B // Notification command
|
|
109
|
+
|
|
110
|
+
// Constants for text wall display
|
|
111
|
+
private const val TEXT_COMMAND = 0x4E // Text command
|
|
112
|
+
private const val DISPLAY_WIDTH = 488
|
|
113
|
+
private const val DISPLAY_USE_WIDTH = 488 // How much of the display to use
|
|
114
|
+
private const val FONT_MULTIPLIER = 1 / 50.0f
|
|
115
|
+
private const val OLD_FONT_SIZE = 21 // Font size
|
|
116
|
+
private const val FONT_DIVIDER = 2.0f
|
|
117
|
+
private const val LINES_PER_SCREEN = 5 // Lines per screen
|
|
118
|
+
private const val MAX_CHUNK_SIZE = 176 // Maximum chunk size for BLE packets
|
|
119
|
+
// private static final int INDENT_SPACES = 32; // Number of spaces to indent
|
|
120
|
+
// text
|
|
121
|
+
|
|
122
|
+
// handle white list stuff
|
|
123
|
+
private const val WHITELIST_CMD = 0x04 // Command ID for whitelist
|
|
124
|
+
|
|
125
|
+
// BMP handling
|
|
126
|
+
|
|
127
|
+
// Add these class variables
|
|
128
|
+
private const val BMP_CHUNK_SIZE = 194
|
|
129
|
+
private val GLASSES_ADDRESS = byteArrayOf(0x00, 0x1c, 0x00, 0x00)
|
|
130
|
+
private val END_COMMAND = byteArrayOf(0x20, 0x0d, 0x0e)
|
|
131
|
+
private const val MAX_BMP_RETRY_ATTEMPTS = 10
|
|
132
|
+
private const val BMP_RETRY_DELAY_MS = 1000L
|
|
133
|
+
|
|
134
|
+
// Platform-specific timing (matching Flutter implementation)
|
|
135
|
+
// private static final long ANDROID_CHUNK_DELAY_MS = 5;
|
|
136
|
+
private const val ANDROID_CHUNK_DELAY_MS = 8L
|
|
137
|
+
private const val IOS_CHUNK_DELAY_MS = 8L
|
|
138
|
+
private const val END_COMMAND_TIMEOUT_MS = 3000L
|
|
139
|
+
private const val CRC_COMMAND_TIMEOUT_MS = 3000L
|
|
140
|
+
private const val CHUNK_SEND_TIMEOUT_MS = 5000L
|
|
141
|
+
|
|
142
|
+
// Optimized bitmap display flags
|
|
143
|
+
private const val USE_OPTIMIZED_BITMAP_DISPLAY = true
|
|
144
|
+
private const val USE_PARALLEL_BITMAP_WRITES = true
|
|
145
|
+
|
|
146
|
+
@JvmStatic
|
|
147
|
+
fun savePreferredG1DeviceId(context: Context?, deviceName: String?) {
|
|
148
|
+
Bridge.saveSetting("deviceName", deviceName!!)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@JvmStatic
|
|
152
|
+
fun deleteEvenSharedPreferences(context: Context?) {
|
|
153
|
+
// savePreferredG1DeviceId(context, null);
|
|
154
|
+
// SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_NAME,
|
|
155
|
+
// Context.MODE_PRIVATE);
|
|
156
|
+
// prefs.edit().clear().apply();
|
|
157
|
+
// Bridge.log("G1: Nuked EvenRealities SharedPreferences");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private fun bytesToHex(bytes: ByteArray): String {
|
|
161
|
+
val sb = StringBuilder()
|
|
162
|
+
for (b in bytes) {
|
|
163
|
+
sb.append(String.format("%02X ", b))
|
|
164
|
+
}
|
|
165
|
+
return sb.toString().trim()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private fun bytesToUtf8(bytes: ByteArray): String {
|
|
169
|
+
return String(bytes, StandardCharsets.UTF_8)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Decodes Even G1 serial number to extract style and color information
|
|
174
|
+
*
|
|
175
|
+
* @param serialNumber The full serial number (e.g., "S110LABD020021")
|
|
176
|
+
* @return Array containing [style, color] or ["Unknown", "Unknown"] if invalid
|
|
177
|
+
*/
|
|
178
|
+
@JvmStatic
|
|
179
|
+
fun decodeEvenG1SerialNumber(serialNumber: String?): Array<String> {
|
|
180
|
+
if (serialNumber == null || serialNumber.length < 6) {
|
|
181
|
+
return arrayOf("Unknown", "Unknown")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Style mapping: 3rd character (index 2)
|
|
185
|
+
val style: String
|
|
186
|
+
when (serialNumber[1]) {
|
|
187
|
+
'0' ->
|
|
188
|
+
style = "Round"
|
|
189
|
+
'1' ->
|
|
190
|
+
style = "Rectangular"
|
|
191
|
+
else ->
|
|
192
|
+
style = "Round"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Color mapping: 5th character (index 4)
|
|
196
|
+
val color: String
|
|
197
|
+
when (serialNumber[4]) {
|
|
198
|
+
'A' ->
|
|
199
|
+
color = "Grey"
|
|
200
|
+
'B' ->
|
|
201
|
+
color = "Brown"
|
|
202
|
+
'C' ->
|
|
203
|
+
color = "Green"
|
|
204
|
+
else ->
|
|
205
|
+
color = "Grey"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return arrayOf(style, color)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private var heartbeatCount = 0
|
|
213
|
+
private var micBeatCount = 0
|
|
214
|
+
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
215
|
+
|
|
216
|
+
private var isKilled = false
|
|
217
|
+
|
|
218
|
+
private var context: Context? = null
|
|
219
|
+
private var leftGlassGatt: BluetoothGatt? = null
|
|
220
|
+
private var rightGlassGatt: BluetoothGatt? = null
|
|
221
|
+
private var leftTxChar: BluetoothGattCharacteristic? = null
|
|
222
|
+
private var rightTxChar: BluetoothGattCharacteristic? = null
|
|
223
|
+
private var leftRxChar: BluetoothGattCharacteristic? = null
|
|
224
|
+
private var rightRxChar: BluetoothGattCharacteristic? = null
|
|
225
|
+
// NOTE: named connectionState in G1.java; renamed because Kotlin can't shadow
|
|
226
|
+
// SGCManager's public val connectionState with a private field of another type.
|
|
227
|
+
private var g1ConnectionState = SmartGlassesConnectionState.DISCONNECTED
|
|
228
|
+
// Executor for parallel bitmap operations (declared before init so the
|
|
229
|
+
// constructor body can assign it; in G1.java this field lived in the BMP section)
|
|
230
|
+
private var bitmapExecutor: ExecutorService? = null
|
|
231
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
232
|
+
private val queryBatteryStatusHandler = Handler(Looper.getMainLooper())
|
|
233
|
+
private val sendBrightnessCommandHandler = Handler(Looper.getMainLooper())
|
|
234
|
+
private var connectHandler = Handler(Looper.getMainLooper())
|
|
235
|
+
private var reconnectHandler = Handler(Looper.getMainLooper())
|
|
236
|
+
private var characteristicHandler = Handler(Looper.getMainLooper())
|
|
237
|
+
private val sendSemaphore = Semaphore(1)
|
|
238
|
+
private var isLeftConnected = false
|
|
239
|
+
private var isRightConnected = false
|
|
240
|
+
private var currentSeq = 0
|
|
241
|
+
private var stopper = false
|
|
242
|
+
private var debugStopper = false
|
|
243
|
+
private var shouldUseAutoBrightness = false
|
|
244
|
+
private var updatingScreen = false
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
private var batteryLeft = -1
|
|
248
|
+
private var batteryRight = -1
|
|
249
|
+
private var leftReconnectAttempts = 0
|
|
250
|
+
private var rightReconnectAttempts = 0
|
|
251
|
+
private var reconnectAttempts = 0 // Counts the number of reconnect attempts
|
|
252
|
+
|
|
253
|
+
// heartbeat sender
|
|
254
|
+
private var heartbeatHandler = Handler()
|
|
255
|
+
private var findCompatibleDevicesHandler: Handler? = null
|
|
256
|
+
private var isScanningForCompatibleDevices = false
|
|
257
|
+
private var isScanning = false
|
|
258
|
+
|
|
259
|
+
private var heartbeatRunnable: Runnable? = null
|
|
260
|
+
|
|
261
|
+
// mic heartbeat turn on
|
|
262
|
+
private var micBeatHandler = Handler()
|
|
263
|
+
private var micBeatRunnable: Runnable? = null
|
|
264
|
+
|
|
265
|
+
// white list sender
|
|
266
|
+
private var whiteListHandler = Handler()
|
|
267
|
+
private var whiteListedAlready = false
|
|
268
|
+
|
|
269
|
+
// mic enable Handler
|
|
270
|
+
private var micEnableHandler = Handler()
|
|
271
|
+
private var isMicrophoneEnabled = false // Track current microphone state
|
|
272
|
+
|
|
273
|
+
// notification period sender
|
|
274
|
+
private var notificationHandler = Handler()
|
|
275
|
+
private var notificationRunnable: Runnable? = null
|
|
276
|
+
private var notifysStarted = false
|
|
277
|
+
private var notificationNum = 10
|
|
278
|
+
|
|
279
|
+
// text wall periodic sender
|
|
280
|
+
private var textWallHandler = Handler()
|
|
281
|
+
private var textWallRunnable: Runnable? = null
|
|
282
|
+
private var textWallsStarted = false
|
|
283
|
+
private var textWallNum = 10
|
|
284
|
+
|
|
285
|
+
// pairing logic
|
|
286
|
+
private var isLeftPairing = false
|
|
287
|
+
private var isRightPairing = false
|
|
288
|
+
private var isLeftBonded = false
|
|
289
|
+
private var isRightBonded = false
|
|
290
|
+
private var leftDevice: BluetoothDevice? = null
|
|
291
|
+
private var rightDevice: BluetoothDevice? = null
|
|
292
|
+
private var leftDeviceName: String? = null // Store name separately since BluetoothDevice.getName() can become null
|
|
293
|
+
private var rightDeviceName: String? = null // Store name separately since BluetoothDevice.getName() can become null
|
|
294
|
+
private var preferredG1Id: String? = null
|
|
295
|
+
private var pendingSavedG1LeftName: String? = null
|
|
296
|
+
private var pendingSavedG1RightName: String? = null
|
|
297
|
+
private var savedG1LeftName: String? = null
|
|
298
|
+
private var savedG1RightName: String? = null
|
|
299
|
+
private var preferredG1DeviceId: String? = null
|
|
300
|
+
|
|
301
|
+
// handler to turn off screen
|
|
302
|
+
// Handler goHomeHandler;
|
|
303
|
+
// Runnable goHomeRunnable;
|
|
304
|
+
|
|
305
|
+
// Retry handler
|
|
306
|
+
var retryBondHandler: Handler? = null
|
|
307
|
+
|
|
308
|
+
// remember when we connected
|
|
309
|
+
private var lastConnectionTimestamp = 0L
|
|
310
|
+
|
|
311
|
+
// Handlers for connection timeouts
|
|
312
|
+
private val leftConnectionTimeoutHandler = Handler(Looper.getMainLooper())
|
|
313
|
+
private val rightConnectionTimeoutHandler = Handler(Looper.getMainLooper())
|
|
314
|
+
|
|
315
|
+
// Runnable tasks for handling timeouts
|
|
316
|
+
private var leftConnectionTimeoutRunnable: Runnable? = null
|
|
317
|
+
private var rightConnectionTimeoutRunnable: Runnable? = null
|
|
318
|
+
private var isBondingReceiverRegistered = false
|
|
319
|
+
private var shouldUseGlassesMic = false
|
|
320
|
+
private var lastThingDisplayedWasAnImage = false
|
|
321
|
+
|
|
322
|
+
// Serial number and style/color information
|
|
323
|
+
// NOTE: G1.java declared public fields serialNumber/style/color here, but they were
|
|
324
|
+
// never read or written; in Kotlin they would clash with SGCManager's val properties,
|
|
325
|
+
// so they are omitted.
|
|
326
|
+
|
|
327
|
+
// lock writing until the last write is successful
|
|
328
|
+
// fonts in G1
|
|
329
|
+
private var g1Text: G1Text? = null
|
|
330
|
+
|
|
331
|
+
@Volatile private var lastSendTimestamp = 0L
|
|
332
|
+
private var lc3DecoderPtr = 0L
|
|
333
|
+
|
|
334
|
+
init {
|
|
335
|
+
this.type = DeviceTypes.G1
|
|
336
|
+
this.hasMic = true // G1 has a built-in microphone
|
|
337
|
+
DeviceStore.apply("glasses", "micEnabled", false)
|
|
338
|
+
Bridge.log("G1: G1 constructor")
|
|
339
|
+
this.context = Bridge.getContext()
|
|
340
|
+
loadPairedDeviceNames()
|
|
341
|
+
// goHomeHandler = new Handler();
|
|
342
|
+
// this.smartGlassesDevice = smartGlassesDevice;
|
|
343
|
+
preferredG1DeviceId = DeviceStore.get("bluetooth", "device_name") as String?
|
|
344
|
+
this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
|
345
|
+
this.shouldUseGlassesMic = false
|
|
346
|
+
|
|
347
|
+
// Initialize bitmap executor for parallel operations
|
|
348
|
+
if (USE_PARALLEL_BITMAP_WRITES) {
|
|
349
|
+
bitmapExecutor = Executors.newFixedThreadPool(2)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// setup LC3 decoder
|
|
353
|
+
if (lc3DecoderPtr == 0L) {
|
|
354
|
+
lc3DecoderPtr = Lc3Cpp.initDecoder()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// setup fonts
|
|
358
|
+
g1Text = G1Text()
|
|
359
|
+
DeviceStore.apply("glasses", "caseRemoved", true)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private val leftGattCallback: BluetoothGattCallback = createGattCallback("Left")
|
|
363
|
+
private val rightGattCallback: BluetoothGattCallback = createGattCallback("Right")
|
|
364
|
+
|
|
365
|
+
private fun createGattCallback(side: String): BluetoothGattCallback {
|
|
366
|
+
return object : BluetoothGattCallback() {
|
|
367
|
+
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
|
368
|
+
// Bridge.log("G1: ConnectionStateChanged");
|
|
369
|
+
// Cancel the connection timeout
|
|
370
|
+
if ("Left" == side && leftConnectionTimeoutRunnable != null) {
|
|
371
|
+
leftConnectionTimeoutHandler.removeCallbacks(leftConnectionTimeoutRunnable!!)
|
|
372
|
+
leftConnectionTimeoutRunnable = null
|
|
373
|
+
} else if ("Right" == side && rightConnectionTimeoutRunnable != null) {
|
|
374
|
+
rightConnectionTimeoutHandler.removeCallbacks(rightConnectionTimeoutRunnable!!)
|
|
375
|
+
rightConnectionTimeoutRunnable = null
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
379
|
+
|
|
380
|
+
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
|
381
|
+
Bridge.log("G1: " + side + " glass connected, discovering services...")
|
|
382
|
+
if ("Left" == side) {
|
|
383
|
+
isLeftConnected = true
|
|
384
|
+
leftReconnectAttempts = 0
|
|
385
|
+
} else {
|
|
386
|
+
isRightConnected = true
|
|
387
|
+
rightReconnectAttempts = 0
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (isLeftConnected && isRightConnected) {
|
|
391
|
+
stopScan()
|
|
392
|
+
Bridge.log("G1: Both glasses connected. Stopping BLE scan.")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
Bridge.log("G1: Discover services calling...")
|
|
396
|
+
gatt.discoverServices()
|
|
397
|
+
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
|
398
|
+
Bridge.log("G1: Glass disconnected, stopping heartbeats")
|
|
399
|
+
Bridge.log("G1: Entering STATE_DISCONNECTED branch for side: " + side)
|
|
400
|
+
|
|
401
|
+
// Mark both sides as not ready (you could also clear both if one disconnects)
|
|
402
|
+
leftServicesWaiter.setTrue()
|
|
403
|
+
rightServicesWaiter.setTrue()
|
|
404
|
+
Bridge.log("G1: Set leftServicesWaiter and rightServicesWaiter to true.")
|
|
405
|
+
|
|
406
|
+
forceSideDisconnection()
|
|
407
|
+
Bridge.log("G1: Called forceSideDisconnection().")
|
|
408
|
+
|
|
409
|
+
// Stop any periodic transmissions
|
|
410
|
+
stopHeartbeat()
|
|
411
|
+
stopMicBeat()
|
|
412
|
+
sendQueue.clear()
|
|
413
|
+
Bridge.log("G1: Stopped heartbeat and mic beat; cleared sendQueue.")
|
|
414
|
+
|
|
415
|
+
updateConnectionState()
|
|
416
|
+
Bridge.log("G1: Updated connection state after disconnection.")
|
|
417
|
+
|
|
418
|
+
// Compute reconnection delay for both sides (here you could choose the maximum
|
|
419
|
+
// of the two delays or a new delay)
|
|
420
|
+
// long delayLeft = Math.min(BASE_RECONNECT_DELAY_MS * (1L <<
|
|
421
|
+
// leftReconnectAttempts), MAX_RECONNECT_DELAY_MS);
|
|
422
|
+
// long delayRight = Math.min(BASE_RECONNECT_DELAY_MS * (1L <<
|
|
423
|
+
// rightReconnectAttempts), MAX_RECONNECT_DELAY_MS);
|
|
424
|
+
val delay = 2000L // or choose another strategy
|
|
425
|
+
// Bridge.log("G1: Computed delayLeft: " + delayLeft + " ms, delayRight: " +
|
|
426
|
+
// delayRight + " ms. Using delay: " + delay + " ms.");
|
|
427
|
+
|
|
428
|
+
Bridge.log("G1: " +
|
|
429
|
+
side + " glass disconnected. Scheduling reconnection for both glasses in " + delay
|
|
430
|
+
+ " ms (Left attempts: " + leftReconnectAttempts + ", Right attempts: "
|
|
431
|
+
+ rightReconnectAttempts + ")")
|
|
432
|
+
|
|
433
|
+
// if (gatt.getDevice() != null) {
|
|
434
|
+
// // Close the current gatt connection
|
|
435
|
+
// Bridge.log("G1: Closing GATT connection for device: " +
|
|
436
|
+
// gatt.getDevice().getAddress());
|
|
437
|
+
// gatt.disconnect();
|
|
438
|
+
// gatt.close();
|
|
439
|
+
// Bridge.log("G1: GATT connection closed.");
|
|
440
|
+
// } else {
|
|
441
|
+
// Bridge.log("G1: No GATT device available to disconnect.");
|
|
442
|
+
// }
|
|
443
|
+
|
|
444
|
+
// Schedule a reconnection for both devices after the delay
|
|
445
|
+
reconnectHandler.postDelayed({
|
|
446
|
+
Bridge.log("G1: Reconnect handler triggered after delay.")
|
|
447
|
+
if (gatt.device != null && !isKilled) {
|
|
448
|
+
Bridge.log("G1: Reconnecting to both glasses. isKilled = " + isKilled)
|
|
449
|
+
// Assuming you have stored references to both devices:
|
|
450
|
+
if (leftDevice != null) {
|
|
451
|
+
Bridge.log("G1: Attempting to reconnect to leftDevice: " + leftDevice!!.address)
|
|
452
|
+
reconnectToGatt(leftDevice!!)
|
|
453
|
+
} else {
|
|
454
|
+
Bridge.log("G1: Left device reference is null.")
|
|
455
|
+
}
|
|
456
|
+
if (rightDevice != null) {
|
|
457
|
+
Bridge.log("G1: Attempting to reconnect to rightDevice: " + rightDevice!!.address)
|
|
458
|
+
reconnectToGatt(rightDevice!!)
|
|
459
|
+
} else {
|
|
460
|
+
Bridge.log("G1: Right device reference is null.")
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
Bridge.log("G1: Reconnect handler aborted: either no GATT device or system is killed.")
|
|
464
|
+
}
|
|
465
|
+
}, delay)
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
Log.e(TAG, "Unexpected connection state encountered for " + side + " glass: " + newState)
|
|
469
|
+
stopHeartbeat()
|
|
470
|
+
stopMicBeat()
|
|
471
|
+
sendQueue.clear()
|
|
472
|
+
|
|
473
|
+
// Mark both sides as not ready (you could also clear both if one disconnects)
|
|
474
|
+
leftServicesWaiter.setTrue()
|
|
475
|
+
rightServicesWaiter.setTrue()
|
|
476
|
+
|
|
477
|
+
Bridge.log("G1: Stopped heartbeat and mic beat; cleared sendQueue due to connection failure.")
|
|
478
|
+
|
|
479
|
+
Log.e(TAG, side + " glass connection failed with status: " + status)
|
|
480
|
+
if ("Left" == side) {
|
|
481
|
+
isLeftConnected = false
|
|
482
|
+
leftReconnectAttempts++
|
|
483
|
+
if (leftGlassGatt != null) {
|
|
484
|
+
leftGlassGatt!!.disconnect()
|
|
485
|
+
leftGlassGatt!!.close()
|
|
486
|
+
}
|
|
487
|
+
leftGlassGatt = null
|
|
488
|
+
} else {
|
|
489
|
+
isRightConnected = false
|
|
490
|
+
rightReconnectAttempts++
|
|
491
|
+
if (rightGlassGatt != null) {
|
|
492
|
+
rightGlassGatt!!.disconnect()
|
|
493
|
+
rightGlassGatt!!.close()
|
|
494
|
+
}
|
|
495
|
+
rightGlassGatt = null
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
forceSideDisconnection()
|
|
499
|
+
Bridge.log("G1: Called forceSideDisconnection() after connection failure.")
|
|
500
|
+
|
|
501
|
+
// Update connection state and notify frontend of disconnection
|
|
502
|
+
updateConnectionState()
|
|
503
|
+
Bridge.log("G1: Updated connection state after connection failure.")
|
|
504
|
+
|
|
505
|
+
connectHandler.postDelayed({
|
|
506
|
+
Bridge.log("G1: Attempting GATT connection for leftDevice immediately.")
|
|
507
|
+
attemptGattConnection(leftDevice)
|
|
508
|
+
}, 0L)
|
|
509
|
+
|
|
510
|
+
connectHandler.postDelayed({
|
|
511
|
+
Bridge.log("G1: Attempting GATT connection for rightDevice after 2000 ms delay.")
|
|
512
|
+
attemptGattConnection(rightDevice)
|
|
513
|
+
}, 400L)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private fun forceSideDisconnection() {
|
|
518
|
+
Bridge.log("G1: forceSideDisconnection() called for side: " + side)
|
|
519
|
+
// Force disconnection from the other side if necessary
|
|
520
|
+
if ("Left" == side) {
|
|
521
|
+
isLeftConnected = false
|
|
522
|
+
leftReconnectAttempts++
|
|
523
|
+
Bridge.log("G1: Left glass: Marked as disconnected and incremented leftReconnectAttempts to "
|
|
524
|
+
+ leftReconnectAttempts)
|
|
525
|
+
if (leftGlassGatt != null) {
|
|
526
|
+
Bridge.log("G1: Left glass GATT exists. Disconnecting and closing leftGlassGatt.")
|
|
527
|
+
leftGlassGatt!!.disconnect()
|
|
528
|
+
leftGlassGatt!!.close()
|
|
529
|
+
leftGlassGatt = null
|
|
530
|
+
} else {
|
|
531
|
+
Bridge.log("G1: Left glass GATT is already null.")
|
|
532
|
+
}
|
|
533
|
+
// If right is still connected, disconnect it too
|
|
534
|
+
if (rightGlassGatt != null) {
|
|
535
|
+
Bridge.log("G1: Left glass disconnected - forcing disconnection from right glass.")
|
|
536
|
+
rightGlassGatt!!.disconnect()
|
|
537
|
+
rightGlassGatt!!.close()
|
|
538
|
+
rightGlassGatt = null
|
|
539
|
+
isRightConnected = false
|
|
540
|
+
rightReconnectAttempts++
|
|
541
|
+
Bridge.log("G1: Right glass marked as disconnected and rightReconnectAttempts incremented to "
|
|
542
|
+
+ rightReconnectAttempts)
|
|
543
|
+
} else {
|
|
544
|
+
Bridge.log("G1: Right glass GATT already null, no action taken.")
|
|
545
|
+
}
|
|
546
|
+
} else { // side equals "Right"
|
|
547
|
+
isRightConnected = false
|
|
548
|
+
rightReconnectAttempts++
|
|
549
|
+
Bridge.log("G1: Right glass: Marked as disconnected and incremented rightReconnectAttempts to "
|
|
550
|
+
+ rightReconnectAttempts)
|
|
551
|
+
if (rightGlassGatt != null) {
|
|
552
|
+
Bridge.log("G1: Right glass GATT exists. Disconnecting and closing rightGlassGatt.")
|
|
553
|
+
rightGlassGatt!!.disconnect()
|
|
554
|
+
rightGlassGatt!!.close()
|
|
555
|
+
rightGlassGatt = null
|
|
556
|
+
} else {
|
|
557
|
+
Bridge.log("G1: Right glass GATT is already null.")
|
|
558
|
+
}
|
|
559
|
+
// If left is still connected, disconnect it too
|
|
560
|
+
if (leftGlassGatt != null) {
|
|
561
|
+
Bridge.log("G1: Right glass disconnected - forcing disconnection from left glass.")
|
|
562
|
+
leftGlassGatt!!.disconnect()
|
|
563
|
+
leftGlassGatt!!.close()
|
|
564
|
+
leftGlassGatt = null
|
|
565
|
+
isLeftConnected = false
|
|
566
|
+
leftReconnectAttempts++
|
|
567
|
+
Bridge.log("G1: Left glass marked as disconnected and leftReconnectAttempts incremented to "
|
|
568
|
+
+ leftReconnectAttempts)
|
|
569
|
+
} else {
|
|
570
|
+
Bridge.log("G1: Left glass GATT already null, no action taken.")
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
576
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
577
|
+
Handler(Looper.getMainLooper()).post { initG1s(gatt, side) }
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic,
|
|
582
|
+
status: Int) {
|
|
583
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
584
|
+
// Bridge.log("G1: PROC_QUEUE - " + side + " glass write successful");
|
|
585
|
+
} else {
|
|
586
|
+
Bridge.log("G1: " + side + " glass write failed with status: " + status)
|
|
587
|
+
|
|
588
|
+
if (status == 133) {
|
|
589
|
+
Bridge.log("G1: GOT THAT 133 STATUS!")
|
|
590
|
+
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// clear the waiter
|
|
595
|
+
if ("Left" == side) {
|
|
596
|
+
leftWaiter.setFalse()
|
|
597
|
+
} else {
|
|
598
|
+
rightWaiter.setFalse()
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
|
|
603
|
+
Bridge.log("G1: PROC - GOT DESCRIPTOR WRITE: " + status)
|
|
604
|
+
|
|
605
|
+
// clear the waiter
|
|
606
|
+
if ("Left" == side) {
|
|
607
|
+
leftServicesWaiter.setFalse()
|
|
608
|
+
} else {
|
|
609
|
+
rightServicesWaiter.setFalse()
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
|
614
|
+
characteristicHandler.post {
|
|
615
|
+
if (characteristic.uuid == UART_RX_CHAR_UUID) {
|
|
616
|
+
val data = characteristic.value
|
|
617
|
+
val deviceName = gatt.device.name
|
|
618
|
+
if (deviceName == null)
|
|
619
|
+
return@post
|
|
620
|
+
|
|
621
|
+
// Handle MIC audio data
|
|
622
|
+
if (data.isNotEmpty() && (data[0].toInt() and 0xFF) == 0xF1) {
|
|
623
|
+
// Bridge.log("G1: Lc3 Audio data received. Data: " + Arrays.toString(data) + ",
|
|
624
|
+
// from: " + deviceName);
|
|
625
|
+
val seq = data[1].toInt() and 0xFF // Sequence number
|
|
626
|
+
// eg. LC3 to PCM
|
|
627
|
+
val lc3 = Arrays.copyOfRange(data, 2, 202)
|
|
628
|
+
// byte[] pcmData = L3cCpp.decodeLC3(lc3);
|
|
629
|
+
// if (pcmData == null) {
|
|
630
|
+
// throw new IllegalStateException("Failed to decode LC3 data");
|
|
631
|
+
// }
|
|
632
|
+
|
|
633
|
+
if (deviceName.contains("R_")) {
|
|
634
|
+
// Forward raw LC3 to DeviceManager (matches iOS behavior)
|
|
635
|
+
// G1 uses 20-byte LC3 frames (default canonical config)
|
|
636
|
+
if (shouldUseGlassesMic) {
|
|
637
|
+
DeviceManager.getInstance().handleGlassesMicData(lc3, 20)
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
// Bridge.log("G1: Lc3 Audio data received. Seq: " + seq + ", Data: " +
|
|
641
|
+
// Arrays.toString(lc3) + ", from: " + deviceName);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// HEAD UP MOVEMENTS
|
|
645
|
+
else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x02) {
|
|
646
|
+
// Only check head movements from the right sensor
|
|
647
|
+
if (deviceName.contains("R_")) {
|
|
648
|
+
// Check for head down movement - initial F5 02 signal
|
|
649
|
+
Bridge.log("G1: HEAD UP MOVEMENT DETECTED")
|
|
650
|
+
DeviceStore.apply("glasses", "headUp", true)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// HEAD DOWN MOVEMENTS
|
|
654
|
+
else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x03) {
|
|
655
|
+
if (deviceName.contains("R_")) {
|
|
656
|
+
Bridge.log("G1: HEAD DOWN MOVEMENT DETECTED")
|
|
657
|
+
// clearBmpDisplay();
|
|
658
|
+
DeviceStore.apply("glasses", "headUp", false)
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// DOUBLE TAP
|
|
662
|
+
// appears to be completely broken - clears the screen - we should not tell
|
|
663
|
+
// people to use the touchpads yet til this is fixed
|
|
664
|
+
// else if (data.length > 1 && (data[0] & 0xFF) == 0xF5 && ((data[1] & 0xFF) ==
|
|
665
|
+
// 0x20) || ((data[1] & 0xFF) == 0x00)) {
|
|
666
|
+
// boolean isRight = deviceName.contains("R_");
|
|
667
|
+
// Bridge.log("G1: GOT DOUBLE TAP from isRight?: " + isRight);
|
|
668
|
+
// EventBus.getDefault().post(new GlassesTapOutputEvent(2, isRight,
|
|
669
|
+
// System.currentTimeMillis()));
|
|
670
|
+
// }
|
|
671
|
+
// BATTERY RESPONSE
|
|
672
|
+
else if (data.size > 2 && data[0] == 0x2C.toByte() && data[1] == 0x66.toByte()) {
|
|
673
|
+
if (deviceName.contains("L_")) {
|
|
674
|
+
// Bridge.log("G1: LEFT Battery response received");
|
|
675
|
+
batteryLeft = data[2].toInt()
|
|
676
|
+
} else if (deviceName.contains("R_")) {
|
|
677
|
+
// Bridge.log("G1: RIGHT Battery response received");
|
|
678
|
+
batteryRight = data[2].toInt()
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (batteryLeft != -1 && batteryRight != -1) {
|
|
682
|
+
val minBatt = Math.min(batteryLeft, batteryRight)
|
|
683
|
+
// Bridge.log("G1: Minimum Battery Level: " + minBatt);
|
|
684
|
+
// EventBus.getDefault().post(new BatteryLevelEvent(minBatt, false));
|
|
685
|
+
DeviceStore.apply("glasses", "batteryLevel", minBatt)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// CASE REMOVED
|
|
689
|
+
else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5
|
|
690
|
+
&& ((data[1].toInt() and 0xFF) == 0x07 || (data[1].toInt() and 0xFF) == 0x06)) {
|
|
691
|
+
DeviceStore.apply("glasses", "caseRemoved", true)
|
|
692
|
+
Bridge.log("G1: CASE REMOVED")
|
|
693
|
+
}
|
|
694
|
+
// CASE OPEN
|
|
695
|
+
else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x08) {
|
|
696
|
+
DeviceStore.apply("glasses", "caseOpen", true)
|
|
697
|
+
DeviceStore.apply("glasses", "caseRemoved", false)
|
|
698
|
+
// EventBus.getDefault()
|
|
699
|
+
// .post(new CaseEvent(caseBatteryLevel, caseCharging, caseOpen, caseRemoved));
|
|
700
|
+
}
|
|
701
|
+
// CASE CLOSED
|
|
702
|
+
else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x0B) {
|
|
703
|
+
DeviceStore.apply("glasses", "caseOpen", false)
|
|
704
|
+
DeviceStore.apply("glasses", "caseRemoved", false)
|
|
705
|
+
// EventBus.getDefault()
|
|
706
|
+
// .post(new CaseEvent(caseBatteryLevel, caseCharging, caseOpen, caseRemoved));
|
|
707
|
+
}
|
|
708
|
+
// CASE CHARGING STATUS
|
|
709
|
+
else if (data.size > 3 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x0E) {
|
|
710
|
+
DeviceStore.apply("glasses", "caseCharging", (data[2].toInt() and 0xFF) == 0x01)// TODO: verify this is correct
|
|
711
|
+
// EventBus.getDefault()
|
|
712
|
+
// .post(new CaseEvent(caseBatteryLevel, caseCharging, caseOpen, caseRemoved));
|
|
713
|
+
}
|
|
714
|
+
// CASE CHARGING INFO
|
|
715
|
+
else if (data.size > 3 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x0F) {
|
|
716
|
+
val newCaseBatteryLevel = (data[2].toInt() and 0xFF)// TODO: verify this is correct
|
|
717
|
+
DeviceStore.apply("glasses", "caseBatteryLevel", newCaseBatteryLevel)
|
|
718
|
+
// EventBus.getDefault()
|
|
719
|
+
// .post(new CaseEvent(caseBatteryLevel, caseCharging, caseOpen, caseRemoved));
|
|
720
|
+
}
|
|
721
|
+
// HEARTBEAT RESPONSE
|
|
722
|
+
else if (data.isNotEmpty() && data[0] == 0x25.toByte()) {
|
|
723
|
+
Bridge.log("G1: Heartbeat response received")
|
|
724
|
+
}
|
|
725
|
+
// TEXT RESPONSE
|
|
726
|
+
else if (data.isNotEmpty() && data[0] == 0x4E.toByte()) {
|
|
727
|
+
// Bridge.log("G1: Text response on side " + (deviceName.contains("L_") ? "Left" : "Right")
|
|
728
|
+
// + " was: " + ((data.length > 1 && (data[1] & 0xFF) == 0xC9) ? "SUCCEED" : "FAIL"));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Handle other non-audio responses
|
|
732
|
+
else {
|
|
733
|
+
// Bridge.log("G1: PROC - Received other Even Realities response: " + bytesToHex(data) + ", from: "
|
|
734
|
+
// + deviceName);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// clear the waiter
|
|
738
|
+
// if ((data.length > 1 && (data[1] & 0xFF) == 0xC9)){
|
|
739
|
+
// if (deviceName.contains("L_")) {
|
|
740
|
+
// Bridge.log("G1: PROC - clearing LEFT waiter on success");
|
|
741
|
+
// leftWaiter.setFalse();
|
|
742
|
+
// } else {
|
|
743
|
+
// Bridge.log("G1: PROC - clearing RIGHT waiter on success");
|
|
744
|
+
// rightWaiter.setFalse();
|
|
745
|
+
// }
|
|
746
|
+
// }
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private fun initG1s(gatt: BluetoothGatt, side: String) {
|
|
755
|
+
gatt.requestMtu(251) // Request a higher MTU size
|
|
756
|
+
Bridge.log("G1: Requested MTU size: 251")
|
|
757
|
+
|
|
758
|
+
val uartService = gatt.getService(UART_SERVICE_UUID)
|
|
759
|
+
|
|
760
|
+
if (uartService != null) {
|
|
761
|
+
val txChar = uartService.getCharacteristic(UART_TX_CHAR_UUID)
|
|
762
|
+
val rxChar = uartService.getCharacteristic(UART_RX_CHAR_UUID)
|
|
763
|
+
|
|
764
|
+
if (txChar != null) {
|
|
765
|
+
if ("Left" == side)
|
|
766
|
+
leftTxChar = txChar
|
|
767
|
+
else
|
|
768
|
+
rightTxChar = txChar
|
|
769
|
+
// enableNotification(gatt, txChar, side);
|
|
770
|
+
// txChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
|
|
771
|
+
Bridge.log("G1: " + side + " glass TX characteristic found")
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (rxChar != null) {
|
|
775
|
+
if ("Left" == side)
|
|
776
|
+
leftRxChar = rxChar
|
|
777
|
+
else
|
|
778
|
+
rightRxChar = rxChar
|
|
779
|
+
enableNotification(gatt, rxChar, side)
|
|
780
|
+
// rxChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
|
|
781
|
+
Bridge.log("G1: " + side + " glass RX characteristic found")
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Mark as connected but wait for setup below to update connection state
|
|
785
|
+
if ("Left" == side) {
|
|
786
|
+
isLeftConnected = true
|
|
787
|
+
Bridge.log("G1: PROC_QUEUE - left side setup complete")
|
|
788
|
+
|
|
789
|
+
// Manufacturer data decoding moved to connection start
|
|
790
|
+
} else {
|
|
791
|
+
isRightConnected = true
|
|
792
|
+
// Bridge.log("G1: PROC_QUEUE - right side setup complete");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// setup the G1s
|
|
796
|
+
if (isLeftConnected && isRightConnected) {
|
|
797
|
+
Bridge.log("G1: Sending firmware request Command")
|
|
798
|
+
sendDataSequentially(byteArrayOf(0x6E.toByte(), 0x74.toByte()))
|
|
799
|
+
|
|
800
|
+
Bridge.log("G1: Sending init 0x4D Command")
|
|
801
|
+
sendDataSequentially(byteArrayOf(0x4D.toByte(), 0xFB.toByte())) // told this is only left
|
|
802
|
+
|
|
803
|
+
Bridge.log("G1: Sending turn off wear detection command")
|
|
804
|
+
sendDataSequentially(byteArrayOf(0x27.toByte(), 0x00.toByte()))
|
|
805
|
+
|
|
806
|
+
Bridge.log("G1: Sending turn off silent mode Command")
|
|
807
|
+
sendDataSequentially(byteArrayOf(0x03.toByte(), 0x0A.toByte()))
|
|
808
|
+
|
|
809
|
+
// debug command
|
|
810
|
+
// Bridge.log("G1: Sending debug 0xF4 Command");
|
|
811
|
+
// sendDataSequentially(new byte[]{(byte) 0xF4, (byte) 0x01});
|
|
812
|
+
|
|
813
|
+
// no longer need to be staggered as we fixed the sender
|
|
814
|
+
// do first battery status query
|
|
815
|
+
queryBatteryStatusHandler.postDelayed({ queryBatteryStatus() }, 10L)
|
|
816
|
+
|
|
817
|
+
// setup brightness
|
|
818
|
+
val brightnessValue = (DeviceStore.get("bluetooth", "brightness") as Number).toInt()
|
|
819
|
+
val shouldUseAutoBrightness = DeviceStore.get("bluetooth", "auto_brightness") as Boolean
|
|
820
|
+
sendBrightnessCommandHandler
|
|
821
|
+
.postDelayed({ sendBrightnessCommand(brightnessValue, shouldUseAutoBrightness) }, 10L)
|
|
822
|
+
|
|
823
|
+
// MIC state is handled by DeviceManager.updateMicState() after reconnection
|
|
824
|
+
// Don't hardcode mic state here - let DeviceManager restore the user's preference
|
|
825
|
+
|
|
826
|
+
// enable our AugmentOS notification key
|
|
827
|
+
sendWhiteListCommand(10)
|
|
828
|
+
|
|
829
|
+
// start heartbeat
|
|
830
|
+
startHeartbeat(10000)
|
|
831
|
+
|
|
832
|
+
// start mic beat
|
|
833
|
+
// startMicBeat(30000);
|
|
834
|
+
|
|
835
|
+
showHomeScreen() // turn on the g1 display
|
|
836
|
+
|
|
837
|
+
updateConnectionState()
|
|
838
|
+
|
|
839
|
+
// start sending debug notifications
|
|
840
|
+
// startPeriodicNotifications(302);
|
|
841
|
+
// start sending debug notifications
|
|
842
|
+
// startPeriodicTextWall(302);
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
Bridge.log("G1: " + side + " glass UART service not found")
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// working on all phones - must keep the delay
|
|
850
|
+
private fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, side: String) {
|
|
851
|
+
Bridge.log("G1: PROC_QUEUE - Starting notification setup for " + side)
|
|
852
|
+
|
|
853
|
+
// Simply enable notifications
|
|
854
|
+
Bridge.log("G1: PROC_QUEUE - setting characteristic notification on side: " + side)
|
|
855
|
+
val result = gatt.setCharacteristicNotification(characteristic, true)
|
|
856
|
+
Bridge.log("G1: PROC_QUEUE - setCharacteristicNotification result for " + side + ": " + result)
|
|
857
|
+
|
|
858
|
+
// Set write type for the characteristic
|
|
859
|
+
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
860
|
+
Bridge.log("G1: PROC_QUEUE - write type set for " + side)
|
|
861
|
+
|
|
862
|
+
// wait
|
|
863
|
+
Bridge.log("G1: PROC_QUEUE - waiting to enable it on this side: " + side)
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
Thread.sleep(500)
|
|
867
|
+
} catch (e: InterruptedException) {
|
|
868
|
+
Bridge.log("G1: Error sending data: " + e.message)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
Bridge.log("G1: PROC_QUEUE - get descriptor on side: " + side)
|
|
872
|
+
val descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)
|
|
873
|
+
if (descriptor != null) {
|
|
874
|
+
Bridge.log("G1: PROC_QUEUE - setting descriptor on side: " + side)
|
|
875
|
+
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
876
|
+
val r_result = gatt.writeDescriptor(descriptor)
|
|
877
|
+
Bridge.log("G1: PROC_QUEUE - set descriptor on side: " + side + " with result: " + r_result)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
private fun updateConnectionState() {
|
|
882
|
+
val previousConnectionState = connectionState
|
|
883
|
+
if (isLeftConnected && isRightConnected) {
|
|
884
|
+
g1ConnectionState = SmartGlassesConnectionState.CONNECTED
|
|
885
|
+
Bridge.log("G1: Both glasses connected")
|
|
886
|
+
lastConnectionTimestamp = System.currentTimeMillis()
|
|
887
|
+
DeviceStore.apply("glasses", "fullyBooted", true)
|
|
888
|
+
DeviceStore.apply("glasses", "connected", true)
|
|
889
|
+
} else if (isLeftConnected || isRightConnected) {
|
|
890
|
+
g1ConnectionState = SmartGlassesConnectionState.CONNECTING
|
|
891
|
+
Bridge.log("G1: One glass connected")
|
|
892
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
893
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
894
|
+
} else {
|
|
895
|
+
g1ConnectionState = SmartGlassesConnectionState.DISCONNECTED
|
|
896
|
+
Bridge.log("G1: No glasses connected")
|
|
897
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
898
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private val bondingReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
|
903
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
904
|
+
val action = intent.action
|
|
905
|
+
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED == action) {
|
|
906
|
+
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
|
907
|
+
|
|
908
|
+
// Use device address to match with stored devices instead of relying on getName()
|
|
909
|
+
// which can be null in the bonding broadcast
|
|
910
|
+
val deviceAddress = device!!.address
|
|
911
|
+
var isLeft = false
|
|
912
|
+
var isRight = false
|
|
913
|
+
var deviceName = device.name
|
|
914
|
+
|
|
915
|
+
// Match by address with stored left/right devices from scanning
|
|
916
|
+
if (leftDevice != null && leftDevice!!.address == deviceAddress) {
|
|
917
|
+
isLeft = true
|
|
918
|
+
// Use stored name string (more reliable than BluetoothDevice.getName())
|
|
919
|
+
if (deviceName == null) {
|
|
920
|
+
deviceName = leftDeviceName
|
|
921
|
+
}
|
|
922
|
+
} else if (rightDevice != null && rightDevice!!.address == deviceAddress) {
|
|
923
|
+
isRight = true
|
|
924
|
+
// Use stored name string (more reliable than BluetoothDevice.getName())
|
|
925
|
+
if (deviceName == null) {
|
|
926
|
+
deviceName = rightDeviceName
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// If we couldn't match this device, it's not one we're pairing with
|
|
931
|
+
if (!isLeft && !isRight) {
|
|
932
|
+
Bridge.log("G1: Bond state changed for unknown device: " + deviceAddress)
|
|
933
|
+
return
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// If name is still null after checking stored devices, log and return
|
|
937
|
+
if (deviceName == null) {
|
|
938
|
+
Bridge.log("G1: Could not determine device name for address: " + deviceAddress)
|
|
939
|
+
return
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
|
|
943
|
+
|
|
944
|
+
if (bondState == BluetoothDevice.BOND_BONDED) {
|
|
945
|
+
Bridge.log("G1: Bonded with device: " + deviceName + " (address: " + deviceAddress + ")")
|
|
946
|
+
if (isLeft) {
|
|
947
|
+
isLeftBonded = true
|
|
948
|
+
isLeftPairing = false
|
|
949
|
+
pendingSavedG1LeftName = deviceName
|
|
950
|
+
} else if (isRight) {
|
|
951
|
+
isRightBonded = true
|
|
952
|
+
isRightPairing = false
|
|
953
|
+
pendingSavedG1RightName = deviceName
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Reset both pairing flags when a device bonds successfully
|
|
957
|
+
// This prevents the other device from being blocked when scan restarts
|
|
958
|
+
isLeftPairing = false
|
|
959
|
+
isRightPairing = false
|
|
960
|
+
|
|
961
|
+
// Restart scan for the next device
|
|
962
|
+
if (!isLeftBonded || !isRightBonded) {
|
|
963
|
+
// if (!(isLeftBonded && !isRightBonded)){// || !doPendingPairingIdsMatch()) {
|
|
964
|
+
Bridge.log("G1: Restarting scan to find remaining device...")
|
|
965
|
+
// Add delay for vendor-specific timing issues (e.g., Motorola devices)
|
|
966
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
967
|
+
startScan()
|
|
968
|
+
}, 500L) // 500ms delay
|
|
969
|
+
} else if (isLeftBonded && isRightBonded && !doPendingPairingIdsMatch()) {
|
|
970
|
+
// We've connected to two different G1s...
|
|
971
|
+
// Let's unpair the right, try to pair to a different one
|
|
972
|
+
isRightBonded = false
|
|
973
|
+
isRightConnected = false
|
|
974
|
+
isRightPairing = false
|
|
975
|
+
pendingSavedG1RightName = null
|
|
976
|
+
Bridge.log("G1: Connected to two different G1s - retry right G1 arm")
|
|
977
|
+
} else {
|
|
978
|
+
Bridge.log("G1: Both devices bonded. Proceeding with connections...")
|
|
979
|
+
savedG1LeftName = pendingSavedG1LeftName
|
|
980
|
+
savedG1RightName = pendingSavedG1RightName
|
|
981
|
+
savePairedDeviceNames()
|
|
982
|
+
stopScan()
|
|
983
|
+
|
|
984
|
+
connectHandler.postDelayed({
|
|
985
|
+
connectToGatt(leftDevice!!)
|
|
986
|
+
}, 0L)
|
|
987
|
+
|
|
988
|
+
connectHandler.postDelayed({
|
|
989
|
+
connectToGatt(rightDevice!!)
|
|
990
|
+
}, 2000L)
|
|
991
|
+
}
|
|
992
|
+
} else if (bondState == BluetoothDevice.BOND_NONE) {
|
|
993
|
+
Bridge.log("G1: Bonding failed for device: " + deviceName + " (address: " + deviceAddress + ")")
|
|
994
|
+
if (isLeft)
|
|
995
|
+
isLeftPairing = false
|
|
996
|
+
if (isRight)
|
|
997
|
+
isRightPairing = false
|
|
998
|
+
|
|
999
|
+
// Restart scanning to retry bonding
|
|
1000
|
+
if (retryBondHandler == null) {
|
|
1001
|
+
retryBondHandler = Handler(Looper.getMainLooper())
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
retryBondHandler!!.postDelayed({
|
|
1005
|
+
Bridge.log("G1: Retrying scan after bond failure...")
|
|
1006
|
+
startScan()
|
|
1007
|
+
}, BOND_RETRY_DELAY_MS)
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
fun doPendingPairingIdsMatch(): Boolean {
|
|
1014
|
+
val leftId = parsePairingIdFromDeviceName(pendingSavedG1LeftName)
|
|
1015
|
+
val rightId = parsePairingIdFromDeviceName(pendingSavedG1RightName)
|
|
1016
|
+
Bridge.log("G1: LeftID: " + leftId)
|
|
1017
|
+
Bridge.log("G1: RightID: " + rightId)
|
|
1018
|
+
|
|
1019
|
+
// ok, HACKY, but if one of them is null, that means that we connected to the
|
|
1020
|
+
// other on a previous connect
|
|
1021
|
+
// this whole function shouldn't matter anymore anyway as we properly filter for
|
|
1022
|
+
// the device name, so it should be fine
|
|
1023
|
+
// in the future, the way to actually check this would be to check the final ID
|
|
1024
|
+
// string, which is the only one guaranteed to be unique
|
|
1025
|
+
if (leftId == null || rightId == null) {
|
|
1026
|
+
return true
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return leftId != null && leftId == rightId
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
fun parsePairingIdFromDeviceName(input: String?): String? {
|
|
1033
|
+
if (input == null || input.isEmpty())
|
|
1034
|
+
return null
|
|
1035
|
+
// Regular expression to match the number after "G1_"
|
|
1036
|
+
val pattern = Pattern.compile("G1_(\\d+)_")
|
|
1037
|
+
val matcher = pattern.matcher(input)
|
|
1038
|
+
|
|
1039
|
+
if (matcher.find()) {
|
|
1040
|
+
return matcher.group(1) // Group 1 contains the number
|
|
1041
|
+
}
|
|
1042
|
+
return null // Return null if no match is found
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private fun savePairedDeviceNames() {
|
|
1046
|
+
// if (savedG1LeftName != null && savedG1RightName != null) {
|
|
1047
|
+
// context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
|
|
1048
|
+
// .edit()
|
|
1049
|
+
// .putString(LEFT_DEVICE_KEY, savedG1LeftName)
|
|
1050
|
+
// .putString(RIGHT_DEVICE_KEY, savedG1RightName)
|
|
1051
|
+
// .apply();
|
|
1052
|
+
// Bridge.log("G1: Saved paired device names: Left=" + savedG1LeftName + ", Right="
|
|
1053
|
+
// + savedG1RightName);
|
|
1054
|
+
// }
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private fun loadPairedDeviceNames() {
|
|
1058
|
+
// SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_NAME,
|
|
1059
|
+
// Context.MODE_PRIVATE);
|
|
1060
|
+
// savedG1LeftName = prefs.getString(LEFT_DEVICE_KEY, null);
|
|
1061
|
+
// savedG1RightName = prefs.getString(RIGHT_DEVICE_KEY, null);
|
|
1062
|
+
// Bridge.log("G1: Loaded paired device names: Left=" + savedG1LeftName + ", Right="
|
|
1063
|
+
// + savedG1RightName);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
private fun connectToGatt(device: BluetoothDevice?) {
|
|
1067
|
+
if (device == null) {
|
|
1068
|
+
Bridge.log("G1: Cannot connect to GATT: device is null")
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
Bridge.log("G1: connectToGatt called for device: " + device.name + " (" + device.address + ")")
|
|
1073
|
+
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
|
1074
|
+
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
|
|
1075
|
+
Bridge.log("G1: Bluetooth is disabled or not available. Cannot reconnect to glasses.")
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Reset the services waiter based on device name
|
|
1080
|
+
if (device.name.contains("_L_")) {
|
|
1081
|
+
Bridge.log("G1: Device identified as left side. Resetting leftServicesWaiter.")
|
|
1082
|
+
leftServicesWaiter.setTrue()
|
|
1083
|
+
} else {
|
|
1084
|
+
Bridge.log("G1: Device identified as right side. Resetting rightServicesWaiter.")
|
|
1085
|
+
rightServicesWaiter.setTrue()
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Establish GATT connection based on device name and current connection state
|
|
1089
|
+
if (device.name.contains("_L_") && leftGlassGatt == null) {
|
|
1090
|
+
Bridge.log("G1: Connecting GATT to left side.")
|
|
1091
|
+
leftGlassGatt = device.connectGatt(context, false, leftGattCallback)
|
|
1092
|
+
isLeftConnected = false // Reset connection state
|
|
1093
|
+
Bridge.log("G1: Left GATT connection initiated. isLeftConnected set to false.")
|
|
1094
|
+
} else if (device.name.contains("_R_") && rightGlassGatt == null && isLeftConnected) {
|
|
1095
|
+
Bridge.log("G1: Connecting GATT to right side.")
|
|
1096
|
+
rightGlassGatt = device.connectGatt(context, false, rightGattCallback)
|
|
1097
|
+
isRightConnected = false // Reset connection state
|
|
1098
|
+
Bridge.log("G1: Right GATT connection initiated. isRightConnected set to false.")
|
|
1099
|
+
} else {
|
|
1100
|
+
Bridge.log("G1: Tried to connect to incorrect or already connected device: " + device.name)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private fun reconnectToGatt(device: BluetoothDevice) {
|
|
1105
|
+
if (isKilled) {
|
|
1106
|
+
return
|
|
1107
|
+
}
|
|
1108
|
+
connectToGatt(device) // Reuse the connectToGatt method
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
private val seenDevices: MutableSet<String?> = HashSet()
|
|
1112
|
+
|
|
1113
|
+
private val modernScanCallback: ScanCallback = object : ScanCallback() {
|
|
1114
|
+
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
1115
|
+
val device = result.device
|
|
1116
|
+
val name = device.name
|
|
1117
|
+
|
|
1118
|
+
// Now you can reference the bluetoothAdapter field if needed:
|
|
1119
|
+
if (!bluetoothAdapter!!.isEnabled()) {
|
|
1120
|
+
Bridge.log("G1: Bluetooth is disabled")
|
|
1121
|
+
return
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Check if G1 arm
|
|
1125
|
+
if (name == null || !name.contains("Even G1_")) {
|
|
1126
|
+
return
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Log all available device information for debugging
|
|
1130
|
+
// Bridge.log("G1: === Device Information ===");
|
|
1131
|
+
// Bridge.log("G1: Device Name: " + name);
|
|
1132
|
+
// Bridge.log("G1: Device Address: " + device.getAddress());
|
|
1133
|
+
// Bridge.log("G1: Device Type: " + device.getType());
|
|
1134
|
+
// Bridge.log("G1: Device Class: " + device.getBluetoothClass());
|
|
1135
|
+
// Bridge.log("G1: Bond State: " + device.getBondState());
|
|
1136
|
+
|
|
1137
|
+
// Try to get additional device information using reflection
|
|
1138
|
+
try {
|
|
1139
|
+
// Try to get the full device name (might contain serial number)
|
|
1140
|
+
val getAliasMethod = device.javaClass.getMethod("getAlias")
|
|
1141
|
+
val alias = getAliasMethod.invoke(device) as String?
|
|
1142
|
+
// add alias to seen device set:
|
|
1143
|
+
if (!seenDevices.contains(alias)) {
|
|
1144
|
+
seenDevices.add(alias)
|
|
1145
|
+
} else {
|
|
1146
|
+
return
|
|
1147
|
+
}
|
|
1148
|
+
Bridge.log("G1: Device Alias: " + alias)
|
|
1149
|
+
} catch (e: Exception) {
|
|
1150
|
+
Bridge.log("G1: Could not get device alias: " + e.message)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Capture manufacturer data for left device during scanning
|
|
1154
|
+
if (name != null && name.contains("_L_") && result.scanRecord != null) {
|
|
1155
|
+
val allManufacturerData = result.scanRecord!!.manufacturerSpecificData
|
|
1156
|
+
for (i in 0 until allManufacturerData.size()) {
|
|
1157
|
+
val parsedDeviceName = parsePairingIdFromDeviceName(name)
|
|
1158
|
+
if (parsedDeviceName != null) {
|
|
1159
|
+
// Bridge.log("G1: Parsed Device Name: " + parsedDeviceName);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
val manufacturerId = allManufacturerData.keyAt(i)
|
|
1163
|
+
val data = allManufacturerData.valueAt(i)
|
|
1164
|
+
// Bridge.log("G1: Left Device Manufacturer ID " + manufacturerId + ": " + bytesToHex(data));
|
|
1165
|
+
|
|
1166
|
+
// Try to decode serial number from this manufacturer data
|
|
1167
|
+
val decodedSerial = decodeSerialFromManufacturerData(data)
|
|
1168
|
+
if (decodedSerial != null) {
|
|
1169
|
+
// Bridge.log("G1: LEFT DEVICE DECODED SERIAL NUMBER from ID " + manufacturerId + ": " + decodedSerial);
|
|
1170
|
+
val decoded = decodeEvenG1SerialNumber(decodedSerial)
|
|
1171
|
+
// Bridge.log("G1: LEFT DEVICE Style: " + decoded[0] + ", Color: " + decoded[1]);
|
|
1172
|
+
|
|
1173
|
+
if (preferredG1DeviceId != null && preferredG1DeviceId == parsedDeviceName) {
|
|
1174
|
+
// Store the information (matching iOS implementation)
|
|
1175
|
+
DeviceStore.apply("glasses", "serialNumber", decodedSerial)
|
|
1176
|
+
DeviceStore.apply("glasses", "style", decoded[0])
|
|
1177
|
+
DeviceStore.apply("glasses", "color", decoded[1])
|
|
1178
|
+
|
|
1179
|
+
// Emit the serial number information to React Native
|
|
1180
|
+
emitSerialNumberInfo(decodedSerial, decoded[0], decoded[1])
|
|
1181
|
+
}
|
|
1182
|
+
break
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Bridge.log("G1: PREFERRED ID: " + preferredG1DeviceId);
|
|
1188
|
+
if (preferredG1DeviceId == null || !name.contains("_" + preferredG1DeviceId + "_")) {
|
|
1189
|
+
// Bridge.log("G1: NOT PAIRED GLASSES");
|
|
1190
|
+
return
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
Bridge.log("G1: FOUND OUR PREFERRED ID: " + preferredG1DeviceId)
|
|
1194
|
+
|
|
1195
|
+
val isLeft = name.contains("_L_")
|
|
1196
|
+
|
|
1197
|
+
// // If we already have saved device names for left/right...
|
|
1198
|
+
// if (savedG1LeftName != null && savedG1RightName != null) {
|
|
1199
|
+
// if (!(name.contains(savedG1LeftName) || name.contains(savedG1RightName))) {
|
|
1200
|
+
// return; // Not a matching device
|
|
1201
|
+
// }
|
|
1202
|
+
// }
|
|
1203
|
+
|
|
1204
|
+
// Identify which side (left/right) and store both device and name
|
|
1205
|
+
if (isLeft) {
|
|
1206
|
+
leftDevice = device
|
|
1207
|
+
leftDeviceName = name // Store name now since getName() can return null later
|
|
1208
|
+
} else {
|
|
1209
|
+
rightDevice = device
|
|
1210
|
+
rightDeviceName = name // Store name now since getName() can return null later
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
val bondState = device.bondState
|
|
1214
|
+
if (bondState != BluetoothDevice.BOND_BONDED) {
|
|
1215
|
+
if (isLeft && !isLeftPairing && !isLeftBonded) {
|
|
1216
|
+
// Stop scan before initiating bond
|
|
1217
|
+
stopScan()
|
|
1218
|
+
// Bridge.log("G1: Bonding with Left Glass...");
|
|
1219
|
+
isLeftPairing = true
|
|
1220
|
+
g1ConnectionState = SmartGlassesConnectionState.BONDING
|
|
1221
|
+
// connectionEvent(g1ConnectionState);
|
|
1222
|
+
bondDevice(device)
|
|
1223
|
+
} else if (!isLeft && !isRightPairing && !isRightBonded) {
|
|
1224
|
+
// Stop scan before initiating bond
|
|
1225
|
+
stopScan()
|
|
1226
|
+
Bridge.log("G1: Attempting to bond with right device. isRightPairing=" + isRightPairing
|
|
1227
|
+
+ ", isRightBonded=" + isRightBonded)
|
|
1228
|
+
isRightPairing = true
|
|
1229
|
+
g1ConnectionState = SmartGlassesConnectionState.BONDING
|
|
1230
|
+
// connectionEvent(g1ConnectionState);
|
|
1231
|
+
bondDevice(device)
|
|
1232
|
+
} else {
|
|
1233
|
+
Bridge.log("G1: Not running bonding - isLeft=" + isLeft + ", isLeftPairing=" + isLeftPairing +
|
|
1234
|
+
", isLeftBonded=" + isLeftBonded + ", isRightPairing=" + isRightPairing +
|
|
1235
|
+
", isRightBonded=" + isRightBonded + " - continuing scan for other side")
|
|
1236
|
+
}
|
|
1237
|
+
} else {
|
|
1238
|
+
// Already bonded
|
|
1239
|
+
if (isLeft) {
|
|
1240
|
+
isLeftBonded = true
|
|
1241
|
+
} else {
|
|
1242
|
+
isRightBonded = true
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Both are bonded => connect to GATT
|
|
1246
|
+
if (leftDevice != null && rightDevice != null && isLeftBonded && isRightBonded) {
|
|
1247
|
+
Bridge.log("G1: Both sides bonded. Ready to connect to GATT.")
|
|
1248
|
+
stopScan()
|
|
1249
|
+
|
|
1250
|
+
connectHandler.postDelayed({
|
|
1251
|
+
attemptGattConnection(leftDevice)
|
|
1252
|
+
}, 0L)
|
|
1253
|
+
|
|
1254
|
+
connectHandler.postDelayed({
|
|
1255
|
+
attemptGattConnection(rightDevice)
|
|
1256
|
+
}, 2000L)
|
|
1257
|
+
} else {
|
|
1258
|
+
Bridge.log("G1: Not running a63dd")
|
|
1259
|
+
Bridge.log("G1: leftBonded=" + isLeftBonded + ", rightBonded=" + isRightBonded)
|
|
1260
|
+
Bridge.log("G1: leftDevice=" + leftDevice + ", rightDevice=" + rightDevice)
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
override fun onScanFailed(errorCode: Int) {
|
|
1266
|
+
Bridge.log("G1: Scan failed with error: " + errorCode)
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
private fun resetAllBondsAndState() {
|
|
1271
|
+
Bridge.log("G1: Resetting ALL bonds and internal state for complete fresh start")
|
|
1272
|
+
|
|
1273
|
+
// Remove both bonds if devices exist
|
|
1274
|
+
if (leftDevice != null) {
|
|
1275
|
+
removeBond(leftDevice!!)
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (rightDevice != null) {
|
|
1279
|
+
removeBond(rightDevice!!)
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Reset all internal state
|
|
1283
|
+
isLeftBonded = false
|
|
1284
|
+
isRightBonded = false
|
|
1285
|
+
isLeftPairing = false
|
|
1286
|
+
isRightPairing = false
|
|
1287
|
+
isLeftConnected = false
|
|
1288
|
+
isRightConnected = false
|
|
1289
|
+
|
|
1290
|
+
// Clear saved device names
|
|
1291
|
+
pendingSavedG1LeftName = null
|
|
1292
|
+
pendingSavedG1RightName = null
|
|
1293
|
+
|
|
1294
|
+
// Close any existing GATT connections
|
|
1295
|
+
if (leftGlassGatt != null) {
|
|
1296
|
+
leftGlassGatt!!.disconnect()
|
|
1297
|
+
leftGlassGatt!!.close()
|
|
1298
|
+
leftGlassGatt = null
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (rightGlassGatt != null) {
|
|
1302
|
+
rightGlassGatt!!.disconnect()
|
|
1303
|
+
rightGlassGatt!!.close()
|
|
1304
|
+
rightGlassGatt = null
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Wait briefly for bond removal to complete
|
|
1308
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
1309
|
+
Bridge.log("G1: Restarting scan after complete bond/state reset")
|
|
1310
|
+
g1ConnectionState = SmartGlassesConnectionState.SCANNING
|
|
1311
|
+
// connectionEvent(g1ConnectionState);
|
|
1312
|
+
startScan()
|
|
1313
|
+
}, 2000L)
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Handles a device with a valid bond
|
|
1318
|
+
*/
|
|
1319
|
+
private fun handleValidBond(device: BluetoothDevice, isLeft: Boolean) {
|
|
1320
|
+
Bridge.log("G1: Handling valid bond for " + (if (isLeft) "left" else "right") + " glass")
|
|
1321
|
+
|
|
1322
|
+
// Update state
|
|
1323
|
+
if (isLeft) {
|
|
1324
|
+
isLeftBonded = true
|
|
1325
|
+
} else {
|
|
1326
|
+
isRightBonded = true
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// If both glasses are bonded, connect to GATT
|
|
1330
|
+
if (leftDevice != null && rightDevice != null && isLeftBonded && isRightBonded) {
|
|
1331
|
+
Bridge.log("G1: Both glasses have valid bonds - ready to connect to GATT")
|
|
1332
|
+
|
|
1333
|
+
connectHandler.postDelayed({
|
|
1334
|
+
attemptGattConnection(leftDevice)
|
|
1335
|
+
}, 0L)
|
|
1336
|
+
|
|
1337
|
+
connectHandler.postDelayed({
|
|
1338
|
+
attemptGattConnection(rightDevice)
|
|
1339
|
+
}, 2000L)
|
|
1340
|
+
} else {
|
|
1341
|
+
// Continue scanning for the other glass
|
|
1342
|
+
Bridge.log("Still need to find " + (if (isLeft) "right" else "left") + " glass - resuming scan")
|
|
1343
|
+
startScan()
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Removes an existing bond with a Bluetooth device to force fresh pairing
|
|
1349
|
+
*/
|
|
1350
|
+
private fun removeBond(device: BluetoothDevice?): Boolean {
|
|
1351
|
+
try {
|
|
1352
|
+
if (device == null) {
|
|
1353
|
+
Bridge.log("G1: Cannot remove bond: device is null")
|
|
1354
|
+
return false
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
val method = device.javaClass.getMethod("removeBond")
|
|
1358
|
+
val result = method.invoke(device) as Boolean
|
|
1359
|
+
Bridge.log("G1: Removing bond for device " + device.name + ", result: " + result)
|
|
1360
|
+
return result
|
|
1361
|
+
} catch (e: Exception) {
|
|
1362
|
+
Bridge.log("G1: Error removing bond: " + e.message)
|
|
1363
|
+
return false
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
fun connectToSmartGlasses() {
|
|
1368
|
+
// Register bonding receiver
|
|
1369
|
+
val filter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
|
|
1370
|
+
context!!.registerReceiver(bondingReceiver, filter)
|
|
1371
|
+
isBondingReceiverRegistered = true
|
|
1372
|
+
|
|
1373
|
+
preferredG1DeviceId = DeviceStore.get("bluetooth", "device_name") as String?
|
|
1374
|
+
|
|
1375
|
+
if (!bluetoothAdapter!!.isEnabled()) {
|
|
1376
|
+
return
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Start scanning for devices
|
|
1380
|
+
g1ConnectionState = SmartGlassesConnectionState.SCANNING
|
|
1381
|
+
// connectionEvent(g1ConnectionState);
|
|
1382
|
+
startScan()
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
private fun startScan() {
|
|
1386
|
+
val scanner = bluetoothAdapter!!.bluetoothLeScanner
|
|
1387
|
+
if (scanner == null) {
|
|
1388
|
+
Bridge.log("G1: BluetoothLeScanner not available.")
|
|
1389
|
+
return
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Optionally, define filters if needed
|
|
1393
|
+
val filters = ArrayList<ScanFilter>()
|
|
1394
|
+
// For example, to filter by device name:
|
|
1395
|
+
// filters.add(new ScanFilter.Builder().setDeviceName("Even G1_").build());
|
|
1396
|
+
|
|
1397
|
+
// Set desired scan settings
|
|
1398
|
+
val settings = ScanSettings.Builder()
|
|
1399
|
+
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
|
1400
|
+
.build()
|
|
1401
|
+
|
|
1402
|
+
// Start scanning
|
|
1403
|
+
isScanning = true
|
|
1404
|
+
try {
|
|
1405
|
+
scanner.startScan(filters, settings, modernScanCallback)
|
|
1406
|
+
} catch (e: SecurityException) {
|
|
1407
|
+
// Auto-reconnect paths may fire before BLUETOOTH_SCAN is granted on Android 12+
|
|
1408
|
+
Bridge.log("G1: startScan SecurityException — bluetooth permission missing: " + e.message)
|
|
1409
|
+
isScanning = false
|
|
1410
|
+
g1ConnectionState = SmartGlassesConnectionState.DISCONNECTED
|
|
1411
|
+
return
|
|
1412
|
+
} catch (e: Exception) {
|
|
1413
|
+
Bridge.log("G1: startScan failed: " + e.message)
|
|
1414
|
+
isScanning = false
|
|
1415
|
+
g1ConnectionState = SmartGlassesConnectionState.DISCONNECTED
|
|
1416
|
+
return
|
|
1417
|
+
}
|
|
1418
|
+
Bridge.log("G1: CALL START SCAN - Started scanning for devices...")
|
|
1419
|
+
|
|
1420
|
+
// Ensure scanning state is immediately communicated to UI
|
|
1421
|
+
g1ConnectionState = SmartGlassesConnectionState.SCANNING
|
|
1422
|
+
// connectionEvent(g1ConnectionState);
|
|
1423
|
+
|
|
1424
|
+
// Stop the scan after some time (e.g., 10-15s instead of 60 to avoid
|
|
1425
|
+
// throttling)
|
|
1426
|
+
// handler.postDelayed(() -> stopScan(), 10000);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
override fun stopScan() {
|
|
1430
|
+
val scanner = bluetoothAdapter!!.bluetoothLeScanner
|
|
1431
|
+
if (scanner != null) {
|
|
1432
|
+
scanner.stopScan(modernScanCallback)
|
|
1433
|
+
}
|
|
1434
|
+
isScanning = false
|
|
1435
|
+
Bridge.log("G1: Stopped scanning for devices")
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private fun bondDevice(device: BluetoothDevice) {
|
|
1439
|
+
try {
|
|
1440
|
+
Bridge.log("G1: Attempting to bond with device: " + device.name)
|
|
1441
|
+
val method = device.javaClass.getMethod("createBond")
|
|
1442
|
+
method.invoke(device)
|
|
1443
|
+
} catch (e: Exception) {
|
|
1444
|
+
Bridge.log("G1: Bonding failed: " + e.message)
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
private var rightConnectionRetryRunnable: Runnable? = null
|
|
1449
|
+
|
|
1450
|
+
private fun attemptGattConnection(device: BluetoothDevice?) {
|
|
1451
|
+
// if (!isKilled)
|
|
1452
|
+
|
|
1453
|
+
if (device == null) {
|
|
1454
|
+
Bridge.log("G1: Cannot connect to GATT: Device is null")
|
|
1455
|
+
return
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
val deviceName = device.name
|
|
1459
|
+
if (deviceName == null) {
|
|
1460
|
+
Bridge.log("G1: Skipping null device name: " + device.address
|
|
1461
|
+
+ "... this means something horriffic has occured. Look into this.")
|
|
1462
|
+
return
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
Bridge.log("G1: attemptGattConnection called for device: " + deviceName + " (" + device.address + ")")
|
|
1466
|
+
|
|
1467
|
+
// Check if both devices are bonded before attempting connection
|
|
1468
|
+
if (!isLeftBonded || !isRightBonded) {
|
|
1469
|
+
Bridge.log("G1: Cannot connect to GATT: Both devices are not bonded yet (isLeftBonded: " + isLeftBonded
|
|
1470
|
+
+ ", isRightBonded: " + isRightBonded + ")")
|
|
1471
|
+
return
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
g1ConnectionState = SmartGlassesConnectionState.CONNECTING
|
|
1475
|
+
Bridge.log("G1: Setting g1ConnectionState to CONNECTING. Notifying connectionEvent.")
|
|
1476
|
+
// connectionEvent(g1ConnectionState);
|
|
1477
|
+
|
|
1478
|
+
val isLeftDevice = deviceName.contains("_L_")
|
|
1479
|
+
val isRightDevice = deviceName.contains("_R_")
|
|
1480
|
+
|
|
1481
|
+
if (isLeftDevice) {
|
|
1482
|
+
connectLeftDevice(device)
|
|
1483
|
+
} else if (isRightDevice) {
|
|
1484
|
+
connectRightDevice(device)
|
|
1485
|
+
} else {
|
|
1486
|
+
Bridge.log("G1: Unknown device type: " + deviceName)
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
private fun connectLeftDevice(device: BluetoothDevice) {
|
|
1491
|
+
if (leftGlassGatt == null) {
|
|
1492
|
+
Bridge.log("G1: Attempting GATT connection for Left Glass...")
|
|
1493
|
+
leftGlassGatt = device.connectGatt(context, false, leftGattCallback)
|
|
1494
|
+
isLeftConnected = false
|
|
1495
|
+
Bridge.log("G1: Left GATT connection initiated. isLeftConnected set to false.")
|
|
1496
|
+
} else {
|
|
1497
|
+
Bridge.log("G1: Left Glass GATT already exists")
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
private fun connectRightDevice(device: BluetoothDevice) {
|
|
1502
|
+
// Only connect right after left is fully connected
|
|
1503
|
+
if (isLeftConnected) {
|
|
1504
|
+
if (rightGlassGatt == null) {
|
|
1505
|
+
Bridge.log("G1: Attempting GATT connection for Right Glass...")
|
|
1506
|
+
rightGlassGatt = device.connectGatt(context, false, rightGattCallback)
|
|
1507
|
+
isRightConnected = false
|
|
1508
|
+
Bridge.log("G1: Right GATT connection initiated. isRightConnected set to false.")
|
|
1509
|
+
|
|
1510
|
+
// Cancel any pending retry attempts since we're now connecting
|
|
1511
|
+
if (rightConnectionRetryRunnable != null) {
|
|
1512
|
+
connectHandler.removeCallbacks(rightConnectionRetryRunnable!!)
|
|
1513
|
+
rightConnectionRetryRunnable = null
|
|
1514
|
+
}
|
|
1515
|
+
} else {
|
|
1516
|
+
Bridge.log("G1: Right Glass GATT already exists")
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
Bridge.log("G1: Waiting for left glass before connecting right. Scheduling retry in "
|
|
1520
|
+
+ RIGHT_CONNECTION_RETRY_DELAY + "ms")
|
|
1521
|
+
|
|
1522
|
+
// Cancel any existing retry attempts to avoid duplicate retries
|
|
1523
|
+
if (rightConnectionRetryRunnable != null) {
|
|
1524
|
+
connectHandler.removeCallbacks(rightConnectionRetryRunnable!!)
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Create new retry runnable
|
|
1528
|
+
rightConnectionRetryRunnable = Runnable {
|
|
1529
|
+
if (!isKilled) {
|
|
1530
|
+
Bridge.log("G1: Retrying right glass connection...")
|
|
1531
|
+
attemptGattConnection(device)
|
|
1532
|
+
} else {
|
|
1533
|
+
Bridge.log("G1: Connection cancelled, stopping retry attempts")
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Schedule retry
|
|
1538
|
+
connectHandler.postDelayed(rightConnectionRetryRunnable!!, RIGHT_CONNECTION_RETRY_DELAY)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
private fun createTextPackage(text: String, currentPage: Int, totalPages: Int, screenStatus: Int): ByteArray {
|
|
1543
|
+
val textBytes = text.toByteArray()
|
|
1544
|
+
val buffer = ByteBuffer.allocate(9 + textBytes.size)
|
|
1545
|
+
buffer.put(0x4E.toByte())
|
|
1546
|
+
buffer.put((currentSeq++ and 0xFF).toByte())
|
|
1547
|
+
buffer.put(1.toByte())
|
|
1548
|
+
buffer.put(0.toByte())
|
|
1549
|
+
buffer.put(screenStatus.toByte())
|
|
1550
|
+
buffer.put(0.toByte())
|
|
1551
|
+
buffer.put(0.toByte())
|
|
1552
|
+
buffer.put(currentPage.toByte())
|
|
1553
|
+
buffer.put(totalPages.toByte())
|
|
1554
|
+
buffer.put(textBytes)
|
|
1555
|
+
|
|
1556
|
+
return buffer.array()
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
private fun sendDataSequentially(data: ByteArray) {
|
|
1560
|
+
sendDataSequentially(data, false)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
private fun sendDataSequentially(data: List<ByteArray>) {
|
|
1564
|
+
sendDataSequentially(data, false)
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean) {
|
|
1568
|
+
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
override fun requestPhoto(request: PhotoRequest) {
|
|
1572
|
+
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
override fun startStream(message: MutableMap<String, Any>) {
|
|
1576
|
+
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
override fun stopStream() {
|
|
1580
|
+
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
override fun sendStreamKeepAlive(message: MutableMap<String, Any>) {
|
|
1584
|
+
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {
|
|
1588
|
+
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
override fun stopVideoRecording(requestId: String) {
|
|
1592
|
+
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
override fun sendButtonPhotoSettings() {
|
|
1596
|
+
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
override fun sendButtonVideoRecordingSettings() {
|
|
1600
|
+
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
override fun sendButtonCameraLedSetting() {
|
|
1604
|
+
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
override fun sendCameraFovSetting() {
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
override fun sendButtonMaxRecordingTime() {
|
|
1611
|
+
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
override fun setBrightness(level: Int, autoMode: Boolean) {
|
|
1615
|
+
Bridge.log("G1: setBrightness() - level: " + level + "%, autoMode: " + autoMode)
|
|
1616
|
+
sendBrightnessCommand(level, autoMode)
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
override fun clearDisplay() {
|
|
1620
|
+
Bridge.log("G1: clearDisplay() - sending space")
|
|
1621
|
+
sendTextWall(" ")
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
override fun sendText(text: String) {
|
|
1625
|
+
Bridge.log("G1: sendText() - text: " + text)
|
|
1626
|
+
displayTextWall(text)
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
override fun sendTextWall(text: String) {
|
|
1630
|
+
// Bridge.log("G1: sendTextWall() - text: " + text);
|
|
1631
|
+
displayTextWall(text)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
override fun sendDoubleTextWall(top: String, bottom: String) {
|
|
1635
|
+
Bridge.log("G1: sendDoubleTextWall() - top: " + top + ", bottom: " + bottom)
|
|
1636
|
+
displayDoubleTextWall(top, bottom)
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
override fun displayBitmap(base64ImageData: String, x: Int?, y: Int?, width: Int?, height: Int?): Boolean {
|
|
1640
|
+
try {
|
|
1641
|
+
// Decode base64 to byte array
|
|
1642
|
+
val bmpData = android.util.Base64.decode(base64ImageData, android.util.Base64.DEFAULT)
|
|
1643
|
+
|
|
1644
|
+
if (bmpData == null || bmpData.isEmpty()) {
|
|
1645
|
+
Log.e(TAG, "Failed to decode base64 image data")
|
|
1646
|
+
return false
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// Call internal implementation
|
|
1650
|
+
displayBitmapImage(bmpData)
|
|
1651
|
+
return true
|
|
1652
|
+
} catch (e: Exception) {
|
|
1653
|
+
Log.e(TAG, "Error displaying bitmap from base64", e)
|
|
1654
|
+
return false
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
override fun showDashboard() {
|
|
1659
|
+
exit()
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
override fun ping() {
|
|
1663
|
+
Bridge.log("G1: ping()")
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
override fun dbg1() {
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
override fun dbg2() {
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
override fun setDashboardPosition(height: Int, depth: Int) {
|
|
1673
|
+
Bridge.log("G1: setDashboardPosition() - height: " + height + ", depth: " + depth)
|
|
1674
|
+
sendDashboardPositionCommand(height, depth)
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
override fun setHeadUpAngle(angle: Int) {
|
|
1678
|
+
Bridge.log("G1: setHeadUpAngle() - angle: " + angle)
|
|
1679
|
+
sendHeadUpAngleCommand(angle)
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
override fun getBatteryStatus() {
|
|
1683
|
+
Bridge.log("G1: Requesting battery status")
|
|
1684
|
+
queryBatteryStatus()
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
override fun setSilentMode(enabled: Boolean) {
|
|
1688
|
+
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
override fun exit() {
|
|
1692
|
+
sendExitCommand()
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
override fun sendShutdown() {
|
|
1696
|
+
Bridge.log("sendShutdown - not supported on G1")
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
override fun sendReboot() {
|
|
1700
|
+
Bridge.log("sendReboot - not supported on G1")
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
override fun sendRgbLedControl(requestId: String, packageName: String?, action: String, color: String?, onDurationMs: Int, offDurationMs: Int, count: Int) {
|
|
1704
|
+
Bridge.log("sendRgbLedControl - not supported on G1")
|
|
1705
|
+
Bridge.sendRgbLedControlResponse(requestId, false, "device_not_supported")
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
override fun disconnect() {
|
|
1709
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1710
|
+
destroy()
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
override fun forget() {
|
|
1714
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1715
|
+
destroy()
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
override fun connectById(id: String) {
|
|
1719
|
+
preferredG1DeviceId = id
|
|
1720
|
+
connectToSmartGlasses()
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
override fun getConnectedBluetoothName(): String {
|
|
1724
|
+
// Return left device name if available, otherwise right device name
|
|
1725
|
+
if (leftDevice != null && leftDevice!!.name != null) {
|
|
1726
|
+
return leftDevice!!.name
|
|
1727
|
+
} else if (rightDevice != null && rightDevice!!.name != null) {
|
|
1728
|
+
return rightDevice!!.name
|
|
1729
|
+
}
|
|
1730
|
+
return ""
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
override fun requestWifiScan() {
|
|
1734
|
+
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
override fun sendWifiCredentials(ssid: String, password: String) {
|
|
1738
|
+
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
override fun forgetWifiNetwork(ssid: String) {
|
|
1742
|
+
// G1 doesn't support WiFi
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
override fun sendHotspotState(enabled: Boolean) {
|
|
1746
|
+
// G1 doesn't support hotspot
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
override fun sendUserEmailToGlasses(email: String) {
|
|
1750
|
+
// G1 doesn't support user email (no ASG client)
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
override fun sendIncidentId(incidentId: String, apiBaseUrl: String?) {
|
|
1754
|
+
// G1 doesn't support incident reporting (no ASG client)
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
override fun queryGalleryStatus() {
|
|
1758
|
+
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
override fun sendGalleryMode() {
|
|
1762
|
+
// G1 doesn't have a built-in camera/gallery system
|
|
1763
|
+
Bridge.log("G1: sendGalleryModeActive - not supported on G1")
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
override fun requestVersionInfo() {
|
|
1767
|
+
// G1 doesn't support version info requests
|
|
1768
|
+
Bridge.log("G1: requestVersionInfo - not supported on G1")
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// private void sendDataSequentially(byte[] data, boolean onlyLeft) {
|
|
1772
|
+
// if (stopper) return;
|
|
1773
|
+
// stopper = true;
|
|
1774
|
+
//
|
|
1775
|
+
// new Thread(() -> {
|
|
1776
|
+
// try {
|
|
1777
|
+
// if (leftGlassGatt != null && leftTxChar != null) {
|
|
1778
|
+
// leftTxChar.setValue(data);
|
|
1779
|
+
// leftGlassGatt.writeCharacteristic(leftTxChar);
|
|
1780
|
+
// Thread.sleep(DELAY_BETWEEN_SENDS_MS);
|
|
1781
|
+
// }
|
|
1782
|
+
//
|
|
1783
|
+
// if (!onlyLeft && rightGlassGatt != null && rightTxChar != null) {
|
|
1784
|
+
// rightTxChar.setValue(data);
|
|
1785
|
+
// rightGlassGatt.writeCharacteristic(rightTxChar);
|
|
1786
|
+
// Thread.sleep(DELAY_BETWEEN_SENDS_MS);
|
|
1787
|
+
// }
|
|
1788
|
+
// stopper = false;
|
|
1789
|
+
// } catch (InterruptedException e) {
|
|
1790
|
+
// Bridge.log("G1: Error sending data: " + e.getMessage());
|
|
1791
|
+
// }
|
|
1792
|
+
// }).start();
|
|
1793
|
+
// }
|
|
1794
|
+
|
|
1795
|
+
// Data class to represent a send request
|
|
1796
|
+
private class SendRequest {
|
|
1797
|
+
val data: ByteArray
|
|
1798
|
+
val onlyLeft: Boolean
|
|
1799
|
+
val onlyRight: Boolean
|
|
1800
|
+
var waitTime = -1
|
|
1801
|
+
|
|
1802
|
+
constructor(data: ByteArray, onlyLeft: Boolean, onlyRight: Boolean) {
|
|
1803
|
+
this.data = data
|
|
1804
|
+
this.onlyLeft = onlyLeft
|
|
1805
|
+
this.onlyRight = onlyRight
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
constructor(data: ByteArray, onlyLeft: Boolean, onlyRight: Boolean, waitTime: Int) {
|
|
1809
|
+
this.data = data
|
|
1810
|
+
this.onlyLeft = onlyLeft
|
|
1811
|
+
this.onlyRight = onlyRight
|
|
1812
|
+
this.waitTime = waitTime
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Queue to hold pending requests
|
|
1817
|
+
private val sendQueue: BlockingQueue<Array<SendRequest?>> = LinkedBlockingQueue()
|
|
1818
|
+
|
|
1819
|
+
@Volatile private var isWorkerRunning = false
|
|
1820
|
+
|
|
1821
|
+
// Non-blocking function to add new send request
|
|
1822
|
+
private fun sendDataSequentially(data: ByteArray, onlyLeft: Boolean) {
|
|
1823
|
+
val chunks = arrayOf<SendRequest?>(SendRequest(data, onlyLeft, false))
|
|
1824
|
+
sendQueue.offer(chunks)
|
|
1825
|
+
startWorkerIfNeeded()
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// Non-blocking function to add new send request
|
|
1829
|
+
private fun sendDataSequentially(data: ByteArray, onlyLeft: Boolean, waitTime: Int) {
|
|
1830
|
+
val chunks = arrayOf<SendRequest?>(SendRequest(data, onlyLeft, false, waitTime))
|
|
1831
|
+
sendQueue.offer(chunks)
|
|
1832
|
+
startWorkerIfNeeded()
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Overloaded function to handle multiple chunks (List<byte[]>)
|
|
1836
|
+
private fun sendDataSequentially(data: List<ByteArray>, onlyLeft: Boolean) {
|
|
1837
|
+
sendDataSequentially(data, onlyLeft, false)
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
private fun sendDataSequentially(data: ByteArray, onlyLeft: Boolean, onlyRight: Boolean) {
|
|
1841
|
+
val chunks = arrayOf<SendRequest?>(SendRequest(data, onlyLeft, onlyRight))
|
|
1842
|
+
sendQueue.offer(chunks)
|
|
1843
|
+
startWorkerIfNeeded()
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
private fun sendDataSequentially(data: ByteArray, onlyLeft: Boolean, onlyRight: Boolean, waitTime: Int) {
|
|
1847
|
+
val chunks = arrayOf<SendRequest?>(SendRequest(data, onlyLeft, onlyRight, waitTime))
|
|
1848
|
+
sendQueue.offer(chunks)
|
|
1849
|
+
startWorkerIfNeeded()
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
private fun sendDataSequentially(data: List<ByteArray>, onlyLeft: Boolean, onlyRight: Boolean) {
|
|
1853
|
+
val chunks = arrayOfNulls<SendRequest>(data.size)
|
|
1854
|
+
for (i in 0 until data.size) {
|
|
1855
|
+
chunks[i] = SendRequest(data[i], onlyLeft, onlyRight)
|
|
1856
|
+
}
|
|
1857
|
+
sendQueue.offer(chunks)
|
|
1858
|
+
startWorkerIfNeeded()
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// Start the worker thread if it's not already running
|
|
1862
|
+
@Synchronized
|
|
1863
|
+
private fun startWorkerIfNeeded() {
|
|
1864
|
+
if (!isWorkerRunning) {
|
|
1865
|
+
isWorkerRunning = true
|
|
1866
|
+
Thread(this::processQueue, "EvenRealitiesG1SGCProcessQueue").start()
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Fast send method for bitmap chunks - doesn't wait for write confirmation
|
|
1871
|
+
private fun sendBitmapChunkNoWait(data: ByteArray?, sendLeft: Boolean, sendRight: Boolean) {
|
|
1872
|
+
if (data == null || data.isEmpty())
|
|
1873
|
+
return
|
|
1874
|
+
|
|
1875
|
+
// IMPORTANT: Set write type to NO_RESPONSE for fire-and-forget behavior
|
|
1876
|
+
// This prevents waiting for BLE acknowledgments
|
|
1877
|
+
|
|
1878
|
+
// Send to right glass without waiting
|
|
1879
|
+
if (sendRight && rightGlassGatt != null && rightTxChar != null && isRightConnected) {
|
|
1880
|
+
rightTxChar!!.value = data
|
|
1881
|
+
rightTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
1882
|
+
rightGlassGatt!!.writeCharacteristic(rightTxChar)
|
|
1883
|
+
// Restore default write type
|
|
1884
|
+
rightTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Send to left glass without waiting
|
|
1888
|
+
if (sendLeft && leftGlassGatt != null && leftTxChar != null && isLeftConnected) {
|
|
1889
|
+
leftTxChar!!.value = data
|
|
1890
|
+
leftTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
1891
|
+
leftGlassGatt!!.writeCharacteristic(leftTxChar)
|
|
1892
|
+
// Restore default write type
|
|
1893
|
+
leftTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
class BooleanWaiter {
|
|
1898
|
+
private var flag = true // initially true
|
|
1899
|
+
|
|
1900
|
+
@Synchronized
|
|
1901
|
+
@Throws(InterruptedException::class)
|
|
1902
|
+
fun waitWhileTrue() {
|
|
1903
|
+
while (flag) {
|
|
1904
|
+
(this as java.lang.Object).wait()
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
@Synchronized
|
|
1909
|
+
fun setTrue() {
|
|
1910
|
+
flag = true
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
@Synchronized
|
|
1914
|
+
fun setFalse() {
|
|
1915
|
+
flag = false
|
|
1916
|
+
(this as java.lang.Object).notifyAll()
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
private val leftWaiter = BooleanWaiter()
|
|
1921
|
+
private val rightWaiter = BooleanWaiter()
|
|
1922
|
+
private val leftServicesWaiter = BooleanWaiter()
|
|
1923
|
+
private val rightServicesWaiter = BooleanWaiter()
|
|
1924
|
+
|
|
1925
|
+
private fun processQueue() {
|
|
1926
|
+
// First wait until the services are setup and ready to receive data
|
|
1927
|
+
Bridge.log("G1: PROC_QUEUE - waiting on services waiters")
|
|
1928
|
+
try {
|
|
1929
|
+
leftServicesWaiter.waitWhileTrue()
|
|
1930
|
+
rightServicesWaiter.waitWhileTrue()
|
|
1931
|
+
} catch (e: InterruptedException) {
|
|
1932
|
+
Bridge.log("G1: Interrupted waiting for descriptor writes: " + e.toString())
|
|
1933
|
+
}
|
|
1934
|
+
Bridge.log("G1: PROC_QUEUE - DONE waiting on services waiters")
|
|
1935
|
+
|
|
1936
|
+
while (!isKilled) {
|
|
1937
|
+
try {
|
|
1938
|
+
// Make sure services are ready before processing requests
|
|
1939
|
+
leftServicesWaiter.waitWhileTrue()
|
|
1940
|
+
rightServicesWaiter.waitWhileTrue()
|
|
1941
|
+
|
|
1942
|
+
// This will block until data is available - no CPU spinning!
|
|
1943
|
+
val requests = sendQueue.take()
|
|
1944
|
+
|
|
1945
|
+
for (request in requests) {
|
|
1946
|
+
if (request == null) {
|
|
1947
|
+
isWorkerRunning = false
|
|
1948
|
+
break
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
try {
|
|
1952
|
+
// Force an initial delay so BLE gets all setup
|
|
1953
|
+
val timeSinceConnection = System.currentTimeMillis() - lastConnectionTimestamp
|
|
1954
|
+
if (timeSinceConnection < INITIAL_CONNECTION_DELAY_MS) {
|
|
1955
|
+
Thread.sleep(INITIAL_CONNECTION_DELAY_MS - timeSinceConnection)
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
var leftSuccess = true
|
|
1959
|
+
var rightSuccess = true
|
|
1960
|
+
|
|
1961
|
+
// Start both writes without waiting
|
|
1962
|
+
var leftStarted = false
|
|
1963
|
+
var rightStarted = false
|
|
1964
|
+
|
|
1965
|
+
// Start right glass write (non-blocking)
|
|
1966
|
+
if (!request.onlyLeft && rightGlassGatt != null && rightTxChar != null && isRightConnected) {
|
|
1967
|
+
rightWaiter.setTrue()
|
|
1968
|
+
rightTxChar!!.value = request.data
|
|
1969
|
+
rightSuccess = rightGlassGatt!!.writeCharacteristic(rightTxChar)
|
|
1970
|
+
if (rightSuccess) {
|
|
1971
|
+
rightStarted = true
|
|
1972
|
+
lastSendTimestamp = System.currentTimeMillis()
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// Start left glass write immediately (non-blocking)
|
|
1977
|
+
if (!request.onlyRight && leftGlassGatt != null && leftTxChar != null && isLeftConnected) {
|
|
1978
|
+
leftWaiter.setTrue()
|
|
1979
|
+
leftTxChar!!.value = request.data
|
|
1980
|
+
leftSuccess = leftGlassGatt!!.writeCharacteristic(leftTxChar)
|
|
1981
|
+
if (leftSuccess) {
|
|
1982
|
+
leftStarted = true
|
|
1983
|
+
lastSendTimestamp = System.currentTimeMillis()
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// Now wait for both to complete
|
|
1988
|
+
if (rightStarted) {
|
|
1989
|
+
rightWaiter.waitWhileTrue()
|
|
1990
|
+
}
|
|
1991
|
+
if (leftStarted) {
|
|
1992
|
+
leftWaiter.waitWhileTrue()
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
Thread.sleep(DELAY_BETWEEN_CHUNKS_SEND)
|
|
1996
|
+
|
|
1997
|
+
// If the packet asked us to do a delay, then do it
|
|
1998
|
+
if (request.waitTime != -1) {
|
|
1999
|
+
Thread.sleep(request.waitTime.toLong())
|
|
2000
|
+
}
|
|
2001
|
+
} catch (e: InterruptedException) {
|
|
2002
|
+
Bridge.log("G1: Error sending data: " + e.message)
|
|
2003
|
+
if (isKilled)
|
|
2004
|
+
break
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
} catch (e: InterruptedException) {
|
|
2008
|
+
if (isKilled) {
|
|
2009
|
+
Bridge.log("G1: Process queue thread interrupted - shutting down")
|
|
2010
|
+
break
|
|
2011
|
+
}
|
|
2012
|
+
Bridge.log("G1: Error in queue processing: " + e.message)
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
Bridge.log("G1: Process queue thread exiting")
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// @Override
|
|
2020
|
+
// public void displayReferenceCardSimple(String title, String body, int
|
|
2021
|
+
// lingerTimeMs) {
|
|
2022
|
+
// displayReferenceCardSimple(title, body, lingerTimeMs);
|
|
2023
|
+
// }
|
|
2024
|
+
|
|
2025
|
+
private fun createNotificationJson(appIdentifier: String, title: String, subtitle: String, message: String): String {
|
|
2026
|
+
val currentTime = System.currentTimeMillis() / 1000L // Unix timestamp in seconds
|
|
2027
|
+
val currentDate = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(java.util.Date()) // Date
|
|
2028
|
+
// format
|
|
2029
|
+
// for
|
|
2030
|
+
// 'date'
|
|
2031
|
+
// field
|
|
2032
|
+
|
|
2033
|
+
val ncsNotification = NCSNotification(
|
|
2034
|
+
notificationNum++, // Increment sequence ID for uniqueness
|
|
2035
|
+
1, // type (e.g., 1 = notification type)
|
|
2036
|
+
appIdentifier,
|
|
2037
|
+
title,
|
|
2038
|
+
subtitle,
|
|
2039
|
+
message,
|
|
2040
|
+
currentTime.toInt(), // Cast long to int to match Python
|
|
2041
|
+
currentDate, // Add the current date to the notification
|
|
2042
|
+
"AugmentOS" // display_name
|
|
2043
|
+
)
|
|
2044
|
+
|
|
2045
|
+
val notification = Notification(ncsNotification, "Add")
|
|
2046
|
+
|
|
2047
|
+
val gson = Gson()
|
|
2048
|
+
return gson.toJson(notification)
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
internal class Notification {
|
|
2052
|
+
var ncs_notification: NCSNotification? = null
|
|
2053
|
+
var type: String? = null
|
|
2054
|
+
|
|
2055
|
+
constructor() {
|
|
2056
|
+
// Default constructor
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
constructor(ncs_notification: NCSNotification?, type: String?) {
|
|
2060
|
+
this.ncs_notification = ncs_notification
|
|
2061
|
+
this.type = type
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
internal class NCSNotification(
|
|
2066
|
+
var msg_id: Int,
|
|
2067
|
+
var type: Int,
|
|
2068
|
+
var app_identifier: String,
|
|
2069
|
+
var title: String,
|
|
2070
|
+
var subtitle: String,
|
|
2071
|
+
var message: String,
|
|
2072
|
+
var time_s: Int, // Changed from long to int for consistency
|
|
2073
|
+
var date: String, // Added to match Python's date field
|
|
2074
|
+
var display_name: String
|
|
2075
|
+
)
|
|
2076
|
+
|
|
2077
|
+
private fun createNotificationChunks(json: String): List<ByteArray> {
|
|
2078
|
+
val MAX_CHUNK_SIZE = 176 // 180 - 4 header bytes
|
|
2079
|
+
val jsonBytes = json.toByteArray(StandardCharsets.UTF_8)
|
|
2080
|
+
val totalChunks = Math.ceil(jsonBytes.size.toDouble() / MAX_CHUNK_SIZE).toInt()
|
|
2081
|
+
|
|
2082
|
+
val chunks = ArrayList<ByteArray>()
|
|
2083
|
+
for (i in 0 until totalChunks) {
|
|
2084
|
+
val start = i * MAX_CHUNK_SIZE
|
|
2085
|
+
val end = Math.min(start + MAX_CHUNK_SIZE, jsonBytes.size)
|
|
2086
|
+
val payloadChunk = Arrays.copyOfRange(jsonBytes, start, end)
|
|
2087
|
+
|
|
2088
|
+
// Create the header
|
|
2089
|
+
val header = byteArrayOf(
|
|
2090
|
+
NOTIFICATION.toByte(),
|
|
2091
|
+
0x00, // notify_id (can be updated as needed)
|
|
2092
|
+
totalChunks.toByte(),
|
|
2093
|
+
i.toByte()
|
|
2094
|
+
)
|
|
2095
|
+
|
|
2096
|
+
// Combine header and payload
|
|
2097
|
+
val chunk = ByteBuffer.allocate(header.size + payloadChunk.size)
|
|
2098
|
+
chunk.put(header)
|
|
2099
|
+
chunk.put(payloadChunk)
|
|
2100
|
+
|
|
2101
|
+
chunks.add(chunk.array())
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
return chunks
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
fun displayReferenceCardSimple(title: String, body: String) {
|
|
2108
|
+
if (!isConnected()) {
|
|
2109
|
+
Bridge.log("G1: Not connected to glasses")
|
|
2110
|
+
return
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
if (title.trim().isEmpty() && body.trim().isEmpty()) {
|
|
2114
|
+
if (DeviceManager.getInstance().powerSavingMode) {
|
|
2115
|
+
sendExitCommand()
|
|
2116
|
+
return
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
val chunks = createTextWallChunks(title + "\n\n" + body)
|
|
2121
|
+
for (i in 0 until chunks.size) {
|
|
2122
|
+
val chunk = chunks[i]
|
|
2123
|
+
val isLastChunk = (i == chunks.size - 1)
|
|
2124
|
+
|
|
2125
|
+
if (isLastChunk) {
|
|
2126
|
+
sendDataSequentially(chunk, false)
|
|
2127
|
+
} else {
|
|
2128
|
+
sendDataSequentially(chunk, false, 300)
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
Bridge.log("G1: Send simple reference card")
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
fun destroy() {
|
|
2135
|
+
Bridge.log("G1: EvenRealitiesG1SGC ONDESTROY")
|
|
2136
|
+
showHomeScreen()
|
|
2137
|
+
isKilled = true
|
|
2138
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2139
|
+
|
|
2140
|
+
// Reset battery levels
|
|
2141
|
+
batteryLeft = -1
|
|
2142
|
+
batteryRight = -1
|
|
2143
|
+
DeviceStore.apply("glasses", "batteryLevel", -1)
|
|
2144
|
+
|
|
2145
|
+
// stop BLE scanning
|
|
2146
|
+
stopScan()
|
|
2147
|
+
|
|
2148
|
+
// Shutdown bitmap executor
|
|
2149
|
+
if (bitmapExecutor != null) {
|
|
2150
|
+
bitmapExecutor!!.shutdown()
|
|
2151
|
+
try {
|
|
2152
|
+
if (!bitmapExecutor!!.awaitTermination(1, TimeUnit.SECONDS)) {
|
|
2153
|
+
bitmapExecutor!!.shutdownNow()
|
|
2154
|
+
}
|
|
2155
|
+
} catch (e: InterruptedException) {
|
|
2156
|
+
bitmapExecutor!!.shutdownNow()
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
if (bondingReceiver != null && isBondingReceiverRegistered) {
|
|
2161
|
+
context!!.unregisterReceiver(bondingReceiver)
|
|
2162
|
+
isBondingReceiverRegistered = false
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
if (rightConnectionRetryRunnable != null) {
|
|
2166
|
+
connectHandler.removeCallbacks(rightConnectionRetryRunnable!!)
|
|
2167
|
+
rightConnectionRetryRunnable = null
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// disable the microphone
|
|
2171
|
+
sendSetMicEnabled(false, 0)
|
|
2172
|
+
|
|
2173
|
+
// stop sending heartbeat
|
|
2174
|
+
stopHeartbeat()
|
|
2175
|
+
|
|
2176
|
+
// stop sending micbeat
|
|
2177
|
+
stopMicBeat()
|
|
2178
|
+
|
|
2179
|
+
// Stop periodic notifications
|
|
2180
|
+
stopPeriodicNotifications()
|
|
2181
|
+
|
|
2182
|
+
// Stop periodic text wall
|
|
2183
|
+
// stopPeriodicNotifications();
|
|
2184
|
+
|
|
2185
|
+
if (leftGlassGatt != null) {
|
|
2186
|
+
leftGlassGatt!!.disconnect()
|
|
2187
|
+
leftGlassGatt!!.close()
|
|
2188
|
+
leftGlassGatt = null
|
|
2189
|
+
}
|
|
2190
|
+
if (rightGlassGatt != null) {
|
|
2191
|
+
rightGlassGatt!!.disconnect()
|
|
2192
|
+
rightGlassGatt!!.close()
|
|
2193
|
+
rightGlassGatt = null
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
if (handler != null)
|
|
2197
|
+
handler.removeCallbacksAndMessages(null)
|
|
2198
|
+
if (heartbeatHandler != null && heartbeatRunnable != null)
|
|
2199
|
+
heartbeatHandler.removeCallbacks(heartbeatRunnable!!)
|
|
2200
|
+
if (whiteListHandler != null)
|
|
2201
|
+
whiteListHandler.removeCallbacksAndMessages(null)
|
|
2202
|
+
if (micEnableHandler != null)
|
|
2203
|
+
micEnableHandler.removeCallbacksAndMessages(null)
|
|
2204
|
+
if (notificationHandler != null && notificationRunnable != null)
|
|
2205
|
+
notificationHandler.removeCallbacks(notificationRunnable!!)
|
|
2206
|
+
if (textWallHandler != null && textWallRunnable != null)
|
|
2207
|
+
textWallHandler.removeCallbacks(textWallRunnable!!)
|
|
2208
|
+
// if (goHomeHandler != null)
|
|
2209
|
+
// goHomeHandler.removeCallbacks(goHomeRunnable);
|
|
2210
|
+
if (findCompatibleDevicesHandler != null)
|
|
2211
|
+
findCompatibleDevicesHandler!!.removeCallbacksAndMessages(null)
|
|
2212
|
+
if (connectHandler != null)
|
|
2213
|
+
connectHandler.removeCallbacksAndMessages(null)
|
|
2214
|
+
if (retryBondHandler != null)
|
|
2215
|
+
retryBondHandler!!.removeCallbacksAndMessages(null)
|
|
2216
|
+
if (characteristicHandler != null) {
|
|
2217
|
+
characteristicHandler.removeCallbacksAndMessages(null)
|
|
2218
|
+
}
|
|
2219
|
+
if (reconnectHandler != null) {
|
|
2220
|
+
reconnectHandler.removeCallbacksAndMessages(null)
|
|
2221
|
+
}
|
|
2222
|
+
if (leftConnectionTimeoutHandler != null && leftConnectionTimeoutRunnable != null) {
|
|
2223
|
+
leftConnectionTimeoutHandler.removeCallbacks(leftConnectionTimeoutRunnable!!)
|
|
2224
|
+
}
|
|
2225
|
+
if (rightConnectionTimeoutHandler != null && rightConnectionTimeoutRunnable != null) {
|
|
2226
|
+
rightConnectionTimeoutHandler.removeCallbacks(rightConnectionTimeoutRunnable!!)
|
|
2227
|
+
}
|
|
2228
|
+
if (reconnectHandler != null) {
|
|
2229
|
+
reconnectHandler.removeCallbacksAndMessages(null)
|
|
2230
|
+
}
|
|
2231
|
+
if (queryBatteryStatusHandler != null && queryBatteryStatusHandler != null) {
|
|
2232
|
+
queryBatteryStatusHandler.removeCallbacksAndMessages(null)
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// free LC3 decoder
|
|
2236
|
+
if (lc3DecoderPtr != 0L) {
|
|
2237
|
+
Lc3Cpp.freeDecoder(lc3DecoderPtr)
|
|
2238
|
+
lc3DecoderPtr = 0
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
sendQueue.clear()
|
|
2242
|
+
|
|
2243
|
+
// Add a dummy element to unblock the take() call if needed
|
|
2244
|
+
sendQueue.offer(arrayOfNulls<SendRequest>(0)) // is this needed?
|
|
2245
|
+
|
|
2246
|
+
isWorkerRunning = false
|
|
2247
|
+
|
|
2248
|
+
isLeftConnected = false
|
|
2249
|
+
isRightConnected = false
|
|
2250
|
+
|
|
2251
|
+
// Clear device references and stored names
|
|
2252
|
+
leftDevice = null
|
|
2253
|
+
rightDevice = null
|
|
2254
|
+
leftDeviceName = null
|
|
2255
|
+
rightDeviceName = null
|
|
2256
|
+
|
|
2257
|
+
Bridge.log("G1: EvenRealitiesG1SGC cleanup complete")
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
fun isConnected(): Boolean {
|
|
2261
|
+
return g1ConnectionState == SmartGlassesConnectionState.CONNECTED
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// Remaining methods
|
|
2265
|
+
fun showNaturalLanguageCommandScreen(prompt: String, naturalLanguageInput: String) {
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
fun updateNaturalLanguageCommandScreen(naturalLanguageArgs: String) {
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
fun scrollingTextViewIntermediateText(text: String) {
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
fun scrollingTextViewFinalText(text: String) {
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
fun stopScrollingTextViewMode() {
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
fun displayPromptView(title: String, options: Array<String>) {
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
fun displayTextLine(text: String) {
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
fun displayBitmap(bmp: Bitmap) {
|
|
2287
|
+
try {
|
|
2288
|
+
val bmpBytes = convertBitmapTo1BitBmpBytes(bmp, false)
|
|
2289
|
+
displayBitmapImage(bmpBytes)
|
|
2290
|
+
} catch (e: Exception) {
|
|
2291
|
+
Log.e(TAG, e.message!!)
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
/**
|
|
2296
|
+
* Convert arbitrary image data (PNG/JPEG bytes) to G1-compatible 1-bit BMP format.
|
|
2297
|
+
* Mirrors G2.convertToG2Bmp() for local miniapp bitmap display support.
|
|
2298
|
+
*
|
|
2299
|
+
* @param imageData Raw image bytes (PNG, JPEG, etc.)
|
|
2300
|
+
* @return 1-bit BMP byte array, or null on failure
|
|
2301
|
+
*/
|
|
2302
|
+
fun convertToG1Bmp(imageData: ByteArray): ByteArray? {
|
|
2303
|
+
try {
|
|
2304
|
+
val bmp = BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
|
|
2305
|
+
if (bmp == null) {
|
|
2306
|
+
Bridge.log("G1: convertToG1Bmp - could not decode image data")
|
|
2307
|
+
return null
|
|
2308
|
+
}
|
|
2309
|
+
val bmpBytes = convertBitmapTo1BitBmpBytes(bmp, false)
|
|
2310
|
+
bmp.recycle()
|
|
2311
|
+
Bridge.log("G1: convertToG1Bmp - produced " + bmpBytes.size + " byte BMP")
|
|
2312
|
+
return bmpBytes
|
|
2313
|
+
} catch (e: Exception) {
|
|
2314
|
+
Log.e(TAG, "G1: convertToG1Bmp error: " + e.message)
|
|
2315
|
+
return null
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
fun blankScreen() {
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* Display pre-composed double text wall (two columns) on the glasses.
|
|
2324
|
+
*
|
|
2325
|
+
* NOTE: DisplayProcessor now composes double_text_wall into a single text_wall
|
|
2326
|
+
* with pixel-precise column alignment using ColumnComposer. This method may
|
|
2327
|
+
* not be called anymore for new flows, but is kept for backwards compatibility.
|
|
2328
|
+
*
|
|
2329
|
+
* Column composition is handled by DisplayProcessor in React Native.
|
|
2330
|
+
* This method is a "dumb pipe" - it just combines and sends the text.
|
|
2331
|
+
*/
|
|
2332
|
+
fun displayDoubleTextWall(textTop: String, textBottom: String) {
|
|
2333
|
+
// Text is already composed by DisplayProcessor's ColumnComposer
|
|
2334
|
+
// Just combine and send as a text wall - no custom wrapping logic needed
|
|
2335
|
+
val combinedText = textTop + "\n" + textBottom
|
|
2336
|
+
displayTextWall(combinedText)
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
fun showHomeScreen() {
|
|
2340
|
+
displayTextWall(" ")
|
|
2341
|
+
|
|
2342
|
+
// if (lastThingDisplayedWasAnImage) {
|
|
2343
|
+
// // clearG1Screen();
|
|
2344
|
+
// lastThingDisplayedWasAnImage = false;
|
|
2345
|
+
// }
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
fun clearG1Screen() {
|
|
2349
|
+
Bridge.log("G1: Clearing G1 screen")
|
|
2350
|
+
val exitCommand = byteArrayOf(0x18.toByte())
|
|
2351
|
+
// sendDataSequentially(exitCommand, false);
|
|
2352
|
+
val theClearBitmapOrSomething = loadEmptyBmpFromAssets()
|
|
2353
|
+
val bmp = BitmapJavaUtils.bytesToBitmap(theClearBitmapOrSomething)
|
|
2354
|
+
try {
|
|
2355
|
+
val bmpBytes = convertBitmapTo1BitBmpBytes(bmp, false)
|
|
2356
|
+
displayBitmapImage(bmpBytes)
|
|
2357
|
+
} catch (e: Exception) {
|
|
2358
|
+
Log.e(TAG, "Error displaying clear bitmap: " + e.message)
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
fun displayRowsCard(rowStrings: Array<String>) {
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
fun displayBulletList(title: String, bullets: Array<String>) {
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
fun displayReferenceCardImage(title: String, body: String, imgUrl: String) {
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
fun displayTextWall(a: String) {
|
|
2373
|
+
val chunks = createTextWallChunks(a)
|
|
2374
|
+
sendChunks(chunks)
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
fun setUpdatingScreen(updatingScreen: Boolean) {
|
|
2378
|
+
this.updatingScreen = updatingScreen
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
fun setFontSizes() {
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
// Heartbeat methods
|
|
2385
|
+
private fun constructHeartbeat(): ByteArray {
|
|
2386
|
+
val buffer = ByteBuffer.allocate(6)
|
|
2387
|
+
buffer.put(0x25.toByte())
|
|
2388
|
+
buffer.put(6.toByte())
|
|
2389
|
+
buffer.put((currentSeq and 0xFF).toByte())
|
|
2390
|
+
buffer.put(0x00.toByte())
|
|
2391
|
+
buffer.put(0x04.toByte())
|
|
2392
|
+
buffer.put((currentSeq++ and 0xFF).toByte())
|
|
2393
|
+
return buffer.array()
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
private fun constructBatteryLevelQuery(): ByteArray {
|
|
2397
|
+
val buffer = ByteBuffer.allocate(2)
|
|
2398
|
+
buffer.put(0x2C.toByte()) // Command
|
|
2399
|
+
buffer.put(0x01.toByte()) // use 0x02 for iOS
|
|
2400
|
+
return buffer.array()
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
private fun startHeartbeat(delay: Int) {
|
|
2404
|
+
Bridge.log("G1: Starting heartbeat")
|
|
2405
|
+
if (heartbeatCount > 0)
|
|
2406
|
+
stopHeartbeat()
|
|
2407
|
+
|
|
2408
|
+
heartbeatRunnable = object : Runnable {
|
|
2409
|
+
override fun run() {
|
|
2410
|
+
sendHeartbeat()
|
|
2411
|
+
// sendLoremIpsum();
|
|
2412
|
+
|
|
2413
|
+
// quickRestartG1();
|
|
2414
|
+
|
|
2415
|
+
heartbeatHandler.postDelayed(this, HEARTBEAT_INTERVAL_MS)
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
heartbeatHandler.postDelayed(heartbeatRunnable!!, delay.toLong())
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// periodically send a mic ON request so it never turns off
|
|
2423
|
+
private fun startMicBeat(delay: Int) {
|
|
2424
|
+
Bridge.log("G1: Starting micbeat")
|
|
2425
|
+
if (micBeatCount > 0)
|
|
2426
|
+
stopMicBeat()
|
|
2427
|
+
sendSetMicEnabled(true, 10)
|
|
2428
|
+
|
|
2429
|
+
micBeatRunnable = object : Runnable {
|
|
2430
|
+
override fun run() {
|
|
2431
|
+
Bridge.log("G1: SENDING MIC BEAT")
|
|
2432
|
+
sendSetMicEnabled(shouldUseGlassesMic, 1)
|
|
2433
|
+
micBeatHandler.postDelayed(this, MICBEAT_INTERVAL_MS)
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
micBeatHandler.postDelayed(micBeatRunnable!!, delay.toLong())
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
override fun findCompatibleDevices() {
|
|
2441
|
+
Bridge.log("G1: findCompatibleDevices called")
|
|
2442
|
+
if (isScanningForCompatibleDevices) {
|
|
2443
|
+
Bridge.log("G1: Scan already in progress, skipping...")
|
|
2444
|
+
return
|
|
2445
|
+
}
|
|
2446
|
+
isScanningForCompatibleDevices = true
|
|
2447
|
+
|
|
2448
|
+
val scanner = bluetoothAdapter!!.bluetoothLeScanner
|
|
2449
|
+
if (scanner == null) {
|
|
2450
|
+
Log.e(TAG, "BluetoothLeScanner not available")
|
|
2451
|
+
isScanningForCompatibleDevices = false
|
|
2452
|
+
return
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
val foundDeviceNames = ArrayList<String>()
|
|
2456
|
+
if (findCompatibleDevicesHandler == null) {
|
|
2457
|
+
findCompatibleDevicesHandler = Handler(Looper.getMainLooper())
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// Optional: add filters if you want to narrow the scan
|
|
2461
|
+
val filters = ArrayList<ScanFilter>()
|
|
2462
|
+
val settings = ScanSettings.Builder()
|
|
2463
|
+
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
|
|
2464
|
+
.build()
|
|
2465
|
+
|
|
2466
|
+
// Create a modern ScanCallback instead of the deprecated LeScanCallback
|
|
2467
|
+
val bleScanCallback = object : ScanCallback() {
|
|
2468
|
+
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
2469
|
+
val device = result.device
|
|
2470
|
+
val name = device.name
|
|
2471
|
+
if (name != null && name.contains("Even G1_") && name.contains("_L_")) {
|
|
2472
|
+
synchronized(foundDeviceNames) {
|
|
2473
|
+
if (!foundDeviceNames.contains(name)) {
|
|
2474
|
+
foundDeviceNames.add(name)
|
|
2475
|
+
Bridge.log("Found smart glasses: " + name)
|
|
2476
|
+
var adjustedName = parsePairingIdFromDeviceName(name)
|
|
2477
|
+
// If parsing failed, use the full name as fallback
|
|
2478
|
+
if (adjustedName == null) {
|
|
2479
|
+
adjustedName = name
|
|
2480
|
+
Bridge.log("G1: Failed to parse device ID from name: " + name)
|
|
2481
|
+
}
|
|
2482
|
+
Bridge.sendDiscoveredDevice("Even Realities G1", adjustedName)
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
override fun onBatchScanResults(results: List<ScanResult>) {
|
|
2489
|
+
// If needed, handle batch results here
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
override fun onScanFailed(errorCode: Int) {
|
|
2493
|
+
Log.e(TAG, "BLE scan failed with code: " + errorCode)
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// Start scanning
|
|
2498
|
+
try {
|
|
2499
|
+
scanner.startScan(filters, settings, bleScanCallback)
|
|
2500
|
+
} catch (e: SecurityException) {
|
|
2501
|
+
Bridge.log("G1: findCompatibleDevices startScan SecurityException — bluetooth permission missing: " + e.message)
|
|
2502
|
+
isScanningForCompatibleDevices = false
|
|
2503
|
+
return
|
|
2504
|
+
} catch (e: Exception) {
|
|
2505
|
+
Bridge.log("G1: findCompatibleDevices startScan failed: " + e.message)
|
|
2506
|
+
isScanningForCompatibleDevices = false
|
|
2507
|
+
return
|
|
2508
|
+
}
|
|
2509
|
+
Bridge.log("G1: Started scanning for smart glasses with BluetoothLeScanner...")
|
|
2510
|
+
|
|
2511
|
+
// Stop scanning after 10 seconds (adjust as needed)
|
|
2512
|
+
findCompatibleDevicesHandler!!.postDelayed({
|
|
2513
|
+
scanner.stopScan(bleScanCallback)
|
|
2514
|
+
isScanningForCompatibleDevices = false
|
|
2515
|
+
Bridge.log("G1: Stopped scanning for smart glasses.")
|
|
2516
|
+
// EventBus.getDefault().post(
|
|
2517
|
+
// new GlassesBluetoothSearchStopEvent(
|
|
2518
|
+
// smartGlassesDevice.deviceModelName
|
|
2519
|
+
// )
|
|
2520
|
+
// );
|
|
2521
|
+
}, 10000L)
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
private fun sendWhiteListCommand(delay: Int) {
|
|
2525
|
+
if (whiteListedAlready) {
|
|
2526
|
+
return
|
|
2527
|
+
}
|
|
2528
|
+
whiteListedAlready = true
|
|
2529
|
+
|
|
2530
|
+
Bridge.log("G1: Sending whitelist command")
|
|
2531
|
+
whiteListHandler.postDelayed({
|
|
2532
|
+
val chunks = getWhitelistChunks()
|
|
2533
|
+
sendDataSequentially(chunks, false)
|
|
2534
|
+
// for (byte[] chunk : chunks) {
|
|
2535
|
+
// Bridge.log("G1: Sending this chunk for white list:" + bytesToUtf8(chunk));
|
|
2536
|
+
// sendDataSequentially(chunk, false);
|
|
2537
|
+
//
|
|
2538
|
+
//// // Sleep for 100 milliseconds between sending each chunk
|
|
2539
|
+
//// try {
|
|
2540
|
+
//// Thread.sleep(150);
|
|
2541
|
+
//// } catch (InterruptedException e) {
|
|
2542
|
+
//// e.printStackTrace();
|
|
2543
|
+
//// }
|
|
2544
|
+
// }
|
|
2545
|
+
}, delay.toLong())
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
private fun stopHeartbeat() {
|
|
2549
|
+
Bridge.log("G1: stopHeartbeat()")
|
|
2550
|
+
if (heartbeatHandler != null) {
|
|
2551
|
+
heartbeatHandler.removeCallbacksAndMessages(null)
|
|
2552
|
+
heartbeatHandler.removeCallbacksAndMessages(heartbeatRunnable)
|
|
2553
|
+
heartbeatCount = 0
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
private fun stopMicBeat() {
|
|
2558
|
+
Bridge.log("G1: stopMicBeat()")
|
|
2559
|
+
sendSetMicEnabled(false, 10)
|
|
2560
|
+
if (micBeatHandler != null) {
|
|
2561
|
+
micBeatHandler.removeCallbacksAndMessages(null)
|
|
2562
|
+
micBeatHandler.removeCallbacksAndMessages(micBeatRunnable)
|
|
2563
|
+
micBeatRunnable = null
|
|
2564
|
+
micBeatCount = 0
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
private fun sendHeartbeat() {
|
|
2569
|
+
val heartbeatPacket = constructHeartbeat()
|
|
2570
|
+
// Bridge.log("G1: Sending heartbeat: " + bytesToHex(heartbeatPacket));
|
|
2571
|
+
|
|
2572
|
+
sendDataSequentially(heartbeatPacket, false, 100)
|
|
2573
|
+
|
|
2574
|
+
if (batteryLeft == -1 || batteryRight == -1 || heartbeatCount % 10 == 0) {
|
|
2575
|
+
queryBatteryStatusHandler.postDelayed(this::queryBatteryStatus, 500L)
|
|
2576
|
+
}
|
|
2577
|
+
// queryBatteryStatusHandler.postDelayed(this::queryBatteryStatus, 500);
|
|
2578
|
+
|
|
2579
|
+
heartbeatCount++
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
private fun queryBatteryStatus() {
|
|
2583
|
+
val batteryQueryPacket = constructBatteryLevelQuery()
|
|
2584
|
+
// Bridge.log("G1: Sending battery status query: " +
|
|
2585
|
+
// bytesToHex(batteryQueryPacket));
|
|
2586
|
+
|
|
2587
|
+
sendDataSequentially(batteryQueryPacket, false, 250)
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
fun sendBrightnessCommand(brightness: Int, autoLight: Boolean) {
|
|
2591
|
+
// Validate brightness range
|
|
2592
|
+
|
|
2593
|
+
val validBrightness: Int
|
|
2594
|
+
if (brightness != -1) {
|
|
2595
|
+
validBrightness = (brightness * 63) / 100
|
|
2596
|
+
} else {
|
|
2597
|
+
validBrightness = (30 * 63) / 100
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// Construct the command
|
|
2601
|
+
val buffer = ByteBuffer.allocate(3)
|
|
2602
|
+
buffer.put(0x01.toByte()) // Command
|
|
2603
|
+
buffer.put(validBrightness.toByte()) // Brightness level (0~63)
|
|
2604
|
+
buffer.put((if (autoLight) 1 else 0).toByte()) // Auto light (0 = close, 1 = open)
|
|
2605
|
+
|
|
2606
|
+
sendDataSequentially(buffer.array(), false, 100)
|
|
2607
|
+
|
|
2608
|
+
Bridge.log("G1: Sent auto light brightness command => Brightness: " + brightness + ", Auto Light: "
|
|
2609
|
+
+ (if (autoLight) "Open" else "Close"))
|
|
2610
|
+
|
|
2611
|
+
// send to AugmentOS core
|
|
2612
|
+
// if (autoLight) {
|
|
2613
|
+
// EventBus.getDefault().post(new BrightnessLevelEvent(autoLight));
|
|
2614
|
+
// } else {
|
|
2615
|
+
// EventBus.getDefault().post(new BrightnessLevelEvent(brightness));
|
|
2616
|
+
// }
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
fun sendHeadUpAngleCommand(headUpAngle: Int) {
|
|
2620
|
+
// Validate headUpAngle range (0 ~ 60)
|
|
2621
|
+
var headUpAngleValue = headUpAngle
|
|
2622
|
+
if (headUpAngleValue < 0) {
|
|
2623
|
+
headUpAngleValue = 0
|
|
2624
|
+
} else if (headUpAngleValue > 60) {
|
|
2625
|
+
headUpAngleValue = 60
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
// Construct the command
|
|
2629
|
+
val buffer = ByteBuffer.allocate(3)
|
|
2630
|
+
buffer.put(0x0B.toByte()) // Command for configuring headUp angle
|
|
2631
|
+
buffer.put(headUpAngleValue.toByte()) // Angle value (0~60)
|
|
2632
|
+
buffer.put(0x01.toByte()) // Level (fixed at 0x01)
|
|
2633
|
+
|
|
2634
|
+
sendDataSequentially(buffer.array(), false, 100)
|
|
2635
|
+
|
|
2636
|
+
Bridge.log("G1: Sent headUp angle command => Angle: " + headUpAngleValue)
|
|
2637
|
+
// EventBus.getDefault().post(new HeadUpAngleEvent(headUpAngle));
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
fun sendDashboardPositionCommand(height: Int, depth: Int) {
|
|
2641
|
+
// clamp height and depth to 0-8 and 1-9 respectively:
|
|
2642
|
+
val heightValue = Math.max(0, Math.min(height, 8))
|
|
2643
|
+
val depthValue = Math.max(1, Math.min(depth, 9))
|
|
2644
|
+
|
|
2645
|
+
val globalCounter = 0 // TODO: must be incremented each time this command is sent!
|
|
2646
|
+
|
|
2647
|
+
val buffer = ByteBuffer.allocate(8)
|
|
2648
|
+
buffer.put(0x26.toByte()) // Command for dashboard height
|
|
2649
|
+
buffer.put(0x08.toByte()) // Length
|
|
2650
|
+
buffer.put(0x00.toByte()) // Sequence
|
|
2651
|
+
buffer.put((globalCounter and 0xFF).toByte()) // counter
|
|
2652
|
+
buffer.put(0x02.toByte()) // Fixed value
|
|
2653
|
+
buffer.put(0x01.toByte()) // State ON
|
|
2654
|
+
buffer.put(heightValue.toByte()) // Height value (0-8)
|
|
2655
|
+
buffer.put(depthValue.toByte()) // Depth value (0-9)
|
|
2656
|
+
|
|
2657
|
+
sendDataSequentially(buffer.array(), false, 100)
|
|
2658
|
+
|
|
2659
|
+
Bridge.log("G1: Sent dashboard height/depth command => Height: " + heightValue + ", Depth: " + depthValue)
|
|
2660
|
+
// EventBus.getDefault().post(new DashboardPositionEvent(height, depth));
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
fun updateGlassesBrightness(brightness: Int) {
|
|
2664
|
+
Bridge.log("G1: Updating glasses brightness: " + brightness)
|
|
2665
|
+
sendBrightnessCommand(brightness, false)
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
fun updateGlassesAutoBrightness(autoBrightness: Boolean) {
|
|
2669
|
+
Bridge.log("G1: Updating glasses auto brightness: " + autoBrightness)
|
|
2670
|
+
sendBrightnessCommand(-1, autoBrightness)
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
fun updateGlassesHeadUpAngle(headUpAngle: Int) {
|
|
2674
|
+
sendHeadUpAngleCommand(headUpAngle)
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
fun updateGlassesDepthHeight(depth: Int, height: Int) {
|
|
2678
|
+
sendDashboardPositionCommand(height, depth)
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
fun sendExitCommand() {
|
|
2682
|
+
sendDataSequentially(byteArrayOf(0x18.toByte()), false, 100)
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// microphone stuff
|
|
2686
|
+
fun sendSetMicEnabled(enable: Boolean, delay: Int) {
|
|
2687
|
+
Bridge.log("G1: sendSetMicEnabled(): " + enable)
|
|
2688
|
+
|
|
2689
|
+
isMicrophoneEnabled = enable // Update the state tracker
|
|
2690
|
+
DeviceStore.apply("glasses", "micEnabled", enable)
|
|
2691
|
+
micEnableHandler.postDelayed({
|
|
2692
|
+
if (!isConnected()) {
|
|
2693
|
+
Bridge.log("G1: Tryna start mic: Not connected to glasses")
|
|
2694
|
+
return@postDelayed
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
val command: Byte = 0x0E // Command for MIC control
|
|
2698
|
+
val enableByte = (if (enable) 1 else 0).toByte() // 1 to enable, 0 to disable
|
|
2699
|
+
|
|
2700
|
+
val buffer = ByteBuffer.allocate(2)
|
|
2701
|
+
buffer.put(command)
|
|
2702
|
+
buffer.put(enableByte)
|
|
2703
|
+
|
|
2704
|
+
sendDataSequentially(buffer.array(), false, true, 300) // wait some time to setup the mic
|
|
2705
|
+
Bridge.log("G1: Sent MIC command: " + bytesToHex(buffer.array()))
|
|
2706
|
+
}, delay.toLong())
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// notifications
|
|
2710
|
+
private fun startPeriodicNotifications(delay: Int) {
|
|
2711
|
+
if (notifysStarted) {
|
|
2712
|
+
return
|
|
2713
|
+
}
|
|
2714
|
+
notifysStarted = true
|
|
2715
|
+
|
|
2716
|
+
notificationRunnable = object : Runnable {
|
|
2717
|
+
override fun run() {
|
|
2718
|
+
// Send notification
|
|
2719
|
+
sendPeriodicNotification()
|
|
2720
|
+
|
|
2721
|
+
// Schedule the next notification
|
|
2722
|
+
notificationHandler.postDelayed(this, 12000L)
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// Start the first notification after 5 seconds
|
|
2727
|
+
notificationHandler.postDelayed(notificationRunnable!!, delay.toLong())
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
private fun sendPeriodicNotification() {
|
|
2731
|
+
if (!isConnected()) {
|
|
2732
|
+
Bridge.log("G1: Cannot send notification: Not connected to glasses")
|
|
2733
|
+
return
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// Example notification data (replace with your actual data)
|
|
2737
|
+
// String json = createNotificationJson("com.augment.os", "QuestionAnswerer",
|
|
2738
|
+
// "How much caffeine in dark chocolate?", "25 to 50 grams per piece");
|
|
2739
|
+
val json = createNotificationJson("com.augment.os", "QuestionAnswerer",
|
|
2740
|
+
"How much caffeine in dark chocolate?", "25 to 50 grams per piece")
|
|
2741
|
+
Bridge.log("G1: the JSON to send: " + json)
|
|
2742
|
+
val chunks = createNotificationChunks(json)
|
|
2743
|
+
for (chunk in chunks) {
|
|
2744
|
+
Bridge.log("G1: Sent chunk to glasses: " + bytesToUtf8(chunk))
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// Send each chunk with a short sleep between each send
|
|
2748
|
+
sendDataSequentially(chunks, false)
|
|
2749
|
+
|
|
2750
|
+
Bridge.log("G1: Sent periodic notification")
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
// text wall debug
|
|
2754
|
+
private fun startPeriodicTextWall(delay: Int) {
|
|
2755
|
+
if (textWallsStarted) {
|
|
2756
|
+
return
|
|
2757
|
+
}
|
|
2758
|
+
textWallsStarted = true
|
|
2759
|
+
|
|
2760
|
+
textWallRunnable = object : Runnable {
|
|
2761
|
+
override fun run() {
|
|
2762
|
+
// Send notification
|
|
2763
|
+
sendPeriodicTextWall()
|
|
2764
|
+
|
|
2765
|
+
// Schedule the next notification
|
|
2766
|
+
textWallHandler.postDelayed(this, 12000L)
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// Start the first text wall send after 5 seconds
|
|
2771
|
+
textWallHandler.postDelayed(textWallRunnable!!, delay.toLong())
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
private var textSeqNum = 0 // Sequence number for text packets
|
|
2775
|
+
|
|
2776
|
+
/**
|
|
2777
|
+
* Creates BLE chunks for pre-wrapped text.
|
|
2778
|
+
*
|
|
2779
|
+
* IMPORTANT: Text is expected to come pre-wrapped from the DisplayProcessor in React Native.
|
|
2780
|
+
* This function does NOT perform any text wrapping - it only chunks the text for BLE transmission.
|
|
2781
|
+
* The DisplayProcessor handles all pixel-accurate wrapping using @mentra/display-utils.
|
|
2782
|
+
*
|
|
2783
|
+
* @param text Pre-wrapped text with newlines already in place
|
|
2784
|
+
* @return List of BLE chunks ready for transmission
|
|
2785
|
+
*/
|
|
2786
|
+
private fun createTextWallChunks(text: String): List<ByteArray> {
|
|
2787
|
+
// Text comes pre-wrapped from DisplayProcessor - just chunk it for transmission
|
|
2788
|
+
return chunkTextForTransmission(text)
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
/**
|
|
2792
|
+
* Chunks text into BLE packets for transmission to glasses.
|
|
2793
|
+
* This is a low-level function that handles BLE protocol framing.
|
|
2794
|
+
*
|
|
2795
|
+
* @param text Text to chunk (should already be formatted/wrapped)
|
|
2796
|
+
* @return List of BLE chunks with headers
|
|
2797
|
+
*/
|
|
2798
|
+
private fun chunkTextForTransmission(text: String?): List<ByteArray> {
|
|
2799
|
+
// Handle empty or whitespace-only text by sending at least a space
|
|
2800
|
+
// This ensures the display gets updated/cleared properly
|
|
2801
|
+
val textToSend = if (text == null || text.trim().isEmpty()) " " else text
|
|
2802
|
+
val textBytes = textToSend.toByteArray(StandardCharsets.UTF_8)
|
|
2803
|
+
val totalChunks = Math.ceil(textBytes.size.toDouble() / MAX_CHUNK_SIZE).toInt()
|
|
2804
|
+
|
|
2805
|
+
val allChunks = ArrayList<ByteArray>()
|
|
2806
|
+
for (i in 0 until totalChunks) {
|
|
2807
|
+
val start = i * MAX_CHUNK_SIZE
|
|
2808
|
+
val end = Math.min(start + MAX_CHUNK_SIZE, textBytes.size)
|
|
2809
|
+
val payloadChunk = Arrays.copyOfRange(textBytes, start, end)
|
|
2810
|
+
|
|
2811
|
+
// Create header with protocol specifications
|
|
2812
|
+
val screenStatus: Byte = 0x71 // New content (0x01) + Text Show (0x70)
|
|
2813
|
+
val header = byteArrayOf(
|
|
2814
|
+
TEXT_COMMAND.toByte(), // Command type
|
|
2815
|
+
textSeqNum.toByte(), // Sequence number
|
|
2816
|
+
totalChunks.toByte(), // Total packages
|
|
2817
|
+
i.toByte(), // Current package number
|
|
2818
|
+
screenStatus, // Screen status
|
|
2819
|
+
0x00.toByte(), // new_char_pos0 (high)
|
|
2820
|
+
0x00.toByte(), // new_char_pos1 (low)
|
|
2821
|
+
0x00.toByte(), // Current page number (always 0)
|
|
2822
|
+
0x01.toByte() // Max page number (always 1)
|
|
2823
|
+
)
|
|
2824
|
+
|
|
2825
|
+
// Combine header and payload
|
|
2826
|
+
val chunk = ByteBuffer.allocate(header.size + payloadChunk.size)
|
|
2827
|
+
chunk.put(header)
|
|
2828
|
+
chunk.put(payloadChunk)
|
|
2829
|
+
|
|
2830
|
+
allChunks.add(chunk.array())
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// Increment sequence number for next transmission
|
|
2834
|
+
textSeqNum = (textSeqNum + 1) % 256
|
|
2835
|
+
|
|
2836
|
+
return allChunks
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
private fun calculateSpacesForAlignment(currentWidth: Int, targetPosition: Int, spaceWidth: Int): Int {
|
|
2840
|
+
// Calculate space needed in pixels
|
|
2841
|
+
val pixelsNeeded = targetPosition - currentWidth
|
|
2842
|
+
|
|
2843
|
+
// Calculate spaces needed (with minimum of 1 space for separation)
|
|
2844
|
+
if (pixelsNeeded <= 0) {
|
|
2845
|
+
return 1 // Ensure at least one space between columns
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
// Calculate the exact number of spaces needed
|
|
2849
|
+
val spaces = Math.ceil(pixelsNeeded.toDouble() / spaceWidth).toInt()
|
|
2850
|
+
|
|
2851
|
+
// Cap at a reasonable maximum
|
|
2852
|
+
return Math.min(spaces, 100)
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
private fun sendPeriodicTextWall() {
|
|
2856
|
+
if (!isConnected()) {
|
|
2857
|
+
Bridge.log("G1: Cannot send text wall: Not connected to glasses")
|
|
2858
|
+
return
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
Bridge.log("G1: ^^^^^^^^^^^^^ SENDING DEBUG TEXT WALL")
|
|
2862
|
+
|
|
2863
|
+
// Example text wall content - replace with your actual text content
|
|
2864
|
+
val sampleText = "This is an example of a text wall that will be displayed on the glasses. " +
|
|
2865
|
+
"It demonstrates how text can be split into multiple pages and displayed sequentially. " +
|
|
2866
|
+
"Each page contains multiple lines, and each line is carefully formatted to fit the display width. " +
|
|
2867
|
+
"The text continues across multiple pages, showing how longer content can be handled effectively."
|
|
2868
|
+
|
|
2869
|
+
val chunks = createTextWallChunks(sampleText)
|
|
2870
|
+
|
|
2871
|
+
// Send each chunk with a delay between sends
|
|
2872
|
+
for (chunk in chunks) {
|
|
2873
|
+
sendDataSequentially(chunk)
|
|
2874
|
+
|
|
2875
|
+
// try {
|
|
2876
|
+
// Thread.sleep(150); // 150ms delay between chunks
|
|
2877
|
+
// } catch (InterruptedException e) {
|
|
2878
|
+
// e.printStackTrace();
|
|
2879
|
+
// }
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// Bridge.log("G1: Sent text wall");
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
private fun stopPeriodicNotifications() {
|
|
2886
|
+
if (notificationHandler != null && notificationRunnable != null) {
|
|
2887
|
+
notificationHandler.removeCallbacks(notificationRunnable!!)
|
|
2888
|
+
Bridge.log("G1: Stopped periodic notifications")
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
fun getWhitelistChunks(): List<ByteArray> {
|
|
2893
|
+
// Define the hardcoded whitelist JSON
|
|
2894
|
+
val apps = ArrayList<AppInfo>()
|
|
2895
|
+
apps.add(AppInfo("com.augment.os", "AugmentOS"))
|
|
2896
|
+
val whitelistJson = createWhitelistJson(apps)
|
|
2897
|
+
|
|
2898
|
+
Bridge.log("G1: Creating chunks for hardcoded whitelist: " + whitelistJson)
|
|
2899
|
+
|
|
2900
|
+
// Convert JSON to bytes and split into chunks
|
|
2901
|
+
return createWhitelistChunks(whitelistJson)
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
private fun createWhitelistJson(apps: List<AppInfo>): String {
|
|
2905
|
+
val appList = JSONArray()
|
|
2906
|
+
try {
|
|
2907
|
+
// Add each app to the list
|
|
2908
|
+
for (app in apps) {
|
|
2909
|
+
val appJson = JSONObject()
|
|
2910
|
+
appJson.put("id", app.id)
|
|
2911
|
+
appJson.put("name", app.name)
|
|
2912
|
+
appList.put(appJson)
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
val whitelistJson = JSONObject()
|
|
2916
|
+
whitelistJson.put("calendar_enable", false)
|
|
2917
|
+
whitelistJson.put("call_enable", false)
|
|
2918
|
+
whitelistJson.put("msg_enable", false)
|
|
2919
|
+
whitelistJson.put("ios_mail_enable", false)
|
|
2920
|
+
|
|
2921
|
+
val appObject = JSONObject()
|
|
2922
|
+
appObject.put("list", appList)
|
|
2923
|
+
appObject.put("enable", true)
|
|
2924
|
+
|
|
2925
|
+
whitelistJson.put("app", appObject)
|
|
2926
|
+
|
|
2927
|
+
return whitelistJson.toString()
|
|
2928
|
+
} catch (e: JSONException) {
|
|
2929
|
+
Log.e(TAG, "Error creating whitelist JSON: " + e.message)
|
|
2930
|
+
return "{}"
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// Simple class to hold app info
|
|
2935
|
+
internal class AppInfo(val id: String, val name: String)
|
|
2936
|
+
|
|
2937
|
+
// Helper function to split JSON into chunks
|
|
2938
|
+
private fun createWhitelistChunks(json: String): List<ByteArray> {
|
|
2939
|
+
val MAX_CHUNK_SIZE = 180 - 4 // Reserve space for the header
|
|
2940
|
+
val jsonBytes = json.toByteArray(StandardCharsets.UTF_8)
|
|
2941
|
+
val totalChunks = Math.ceil(jsonBytes.size.toDouble() / MAX_CHUNK_SIZE).toInt()
|
|
2942
|
+
|
|
2943
|
+
val chunks = ArrayList<ByteArray>()
|
|
2944
|
+
for (i in 0 until totalChunks) {
|
|
2945
|
+
val start = i * MAX_CHUNK_SIZE
|
|
2946
|
+
val end = Math.min(start + MAX_CHUNK_SIZE, jsonBytes.size)
|
|
2947
|
+
val payloadChunk = Arrays.copyOfRange(jsonBytes, start, end)
|
|
2948
|
+
|
|
2949
|
+
// Create the header: [WHITELIST_CMD, total_chunks, chunk_index]
|
|
2950
|
+
val header = byteArrayOf(
|
|
2951
|
+
WHITELIST_CMD.toByte(), // Command ID
|
|
2952
|
+
totalChunks.toByte(), // Total number of chunks
|
|
2953
|
+
i.toByte() // Current chunk index
|
|
2954
|
+
)
|
|
2955
|
+
|
|
2956
|
+
// Combine header and payload
|
|
2957
|
+
val buffer = ByteBuffer.allocate(header.size + payloadChunk.size)
|
|
2958
|
+
buffer.put(header)
|
|
2959
|
+
buffer.put(payloadChunk)
|
|
2960
|
+
|
|
2961
|
+
chunks.add(buffer.array())
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
return chunks
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
fun displayCustomContent(content: String) {
|
|
2968
|
+
Bridge.log("G1: DISPLAY CUSTOM CONTENT")
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
private fun sendChunks(chunks: List<ByteArray>) {
|
|
2972
|
+
// Send each chunk with a delay between sends
|
|
2973
|
+
for (chunk in chunks) {
|
|
2974
|
+
// Bridge.log("G1: Sending chunk: " + Arrays.toString(chunk));
|
|
2975
|
+
sendDataSequentially(chunk)
|
|
2976
|
+
|
|
2977
|
+
// try {
|
|
2978
|
+
// Thread.sleep(DELAY_BETWEEN_CHUNKS_SEND); // delay between chunks
|
|
2979
|
+
// } catch (InterruptedException e) {
|
|
2980
|
+
// e.printStackTrace();
|
|
2981
|
+
// }
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// public int DEFAULT_CARD_SHOW_TIME = 6;
|
|
2986
|
+
// public void homeScreenInNSeconds(int n){
|
|
2987
|
+
// if (n == -1){
|
|
2988
|
+
// return;
|
|
2989
|
+
// }
|
|
2990
|
+
//
|
|
2991
|
+
// if (n == 0){
|
|
2992
|
+
// n = DEFAULT_CARD_SHOW_TIME;
|
|
2993
|
+
// }
|
|
2994
|
+
//
|
|
2995
|
+
// //disconnect after slight delay, so our above text gets a chance to show up
|
|
2996
|
+
// goHomeHandler.removeCallbacksAndMessages(goHomeRunnable);
|
|
2997
|
+
// goHomeHandler.removeCallbacksAndMessages(null);
|
|
2998
|
+
// goHomeRunnable = new Runnable() {
|
|
2999
|
+
// @Override
|
|
3000
|
+
// public void run() {
|
|
3001
|
+
// showHomeScreen();
|
|
3002
|
+
// }};
|
|
3003
|
+
// goHomeHandler.postDelayed(goHomeRunnable, n * 1000);
|
|
3004
|
+
// }
|
|
3005
|
+
|
|
3006
|
+
@Volatile private var isSendingBitmap = false
|
|
3007
|
+
|
|
3008
|
+
// Progress callback interface
|
|
3009
|
+
interface BmpProgressCallback {
|
|
3010
|
+
fun onProgress(side: String, offset: Int, chunkIndex: Int, totalSize: Int)
|
|
3011
|
+
|
|
3012
|
+
fun onSuccess(side: String)
|
|
3013
|
+
|
|
3014
|
+
fun onError(side: String, error: String)
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
/**
|
|
3018
|
+
* Inverts BMP pixel data by flipping all bits after the header.
|
|
3019
|
+
* Matches iOS implementation exactly (G1.swift line 1790-1811)
|
|
3020
|
+
*
|
|
3021
|
+
* @param bmpData The original BMP data
|
|
3022
|
+
* @return Inverted BMP data with pixel bits flipped
|
|
3023
|
+
*/
|
|
3024
|
+
private fun invertBmpPixels(bmpData: ByteArray?): ByteArray? {
|
|
3025
|
+
if (bmpData == null || bmpData.size <= 62) {
|
|
3026
|
+
Bridge.log("G1: BMP data too small to contain pixel data")
|
|
3027
|
+
return bmpData
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// BMP header is 62 bytes (14 byte file header + 40 byte DIB header + 8 byte color table)
|
|
3031
|
+
val headerSize = 62
|
|
3032
|
+
val invertedData = ByteArray(bmpData.size)
|
|
3033
|
+
|
|
3034
|
+
// Copy header unchanged
|
|
3035
|
+
System.arraycopy(bmpData, 0, invertedData, 0, headerSize)
|
|
3036
|
+
|
|
3037
|
+
// Invert the pixel data (everything after the header)
|
|
3038
|
+
for (i in headerSize until bmpData.size) {
|
|
3039
|
+
// Invert each byte (flip all bits)
|
|
3040
|
+
invertedData[i] = bmpData[i].toInt().inv().toByte()
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
Bridge.log("G1: Inverted BMP pixels: " + (bmpData.size - headerSize) + " bytes processed")
|
|
3044
|
+
return invertedData
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
fun displayBitmapImage(bmpData: ByteArray?) {
|
|
3048
|
+
displayBitmapImage(bmpData, null)
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
fun displayBitmapImage(bmpData: ByteArray?, callback: BmpProgressCallback?) {
|
|
3052
|
+
// Invert BMP pixels to match iOS implementation
|
|
3053
|
+
val invertedBmpData = invertBmpPixels(bmpData)
|
|
3054
|
+
|
|
3055
|
+
if (USE_OPTIMIZED_BITMAP_DISPLAY) {
|
|
3056
|
+
displayBitmapImageOptimized(invertedBmpData, callback)
|
|
3057
|
+
} else {
|
|
3058
|
+
displayBitmapImageLegacy(invertedBmpData, callback)
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
// Optimized bitmap display using iOS-like approach
|
|
3063
|
+
private fun displayBitmapImageOptimized(bmpData: ByteArray?, callback: BmpProgressCallback?) {
|
|
3064
|
+
Bridge.log("G1: Starting OPTIMIZED BMP display process")
|
|
3065
|
+
|
|
3066
|
+
try {
|
|
3067
|
+
if (bmpData == null || bmpData.isEmpty()) {
|
|
3068
|
+
Log.e(TAG, "Invalid BMP data provided")
|
|
3069
|
+
if (callback != null)
|
|
3070
|
+
callback.onError("both", "Invalid BMP data")
|
|
3071
|
+
return
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
isSendingBitmap = true
|
|
3075
|
+
val startTime = System.currentTimeMillis()
|
|
3076
|
+
|
|
3077
|
+
Bridge.log("G1: Processing BMP data, size: " + bmpData.size + " bytes")
|
|
3078
|
+
val chunks = createBmpChunks(bmpData)
|
|
3079
|
+
Bridge.log("G1: Created " + chunks.size + " chunks")
|
|
3080
|
+
|
|
3081
|
+
// Send chunks using optimized method
|
|
3082
|
+
val chunksSuccess = sendBmpChunksOptimized(chunks, callback)
|
|
3083
|
+
if (!chunksSuccess) {
|
|
3084
|
+
Log.e(TAG, "Failed to send BMP chunks")
|
|
3085
|
+
if (callback != null)
|
|
3086
|
+
callback.onError("both", "Failed to send chunks")
|
|
3087
|
+
return
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// Send end command - this needs confirmation
|
|
3091
|
+
val endSuccess = sendBmpEndCommandOptimized()
|
|
3092
|
+
if (!endSuccess) {
|
|
3093
|
+
Log.e(TAG, "Failed to send BMP end command")
|
|
3094
|
+
if (callback != null)
|
|
3095
|
+
callback.onError("both", "Failed to send end command")
|
|
3096
|
+
return
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
// Send CRC - this needs confirmation
|
|
3100
|
+
val crcSuccess = sendBmpCrcOptimized(bmpData)
|
|
3101
|
+
if (!crcSuccess) {
|
|
3102
|
+
Log.e(TAG, "Failed to send BMP CRC")
|
|
3103
|
+
if (callback != null)
|
|
3104
|
+
callback.onError("both", "CRC check failed")
|
|
3105
|
+
return
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
lastThingDisplayedWasAnImage = true
|
|
3109
|
+
|
|
3110
|
+
val totalTime = System.currentTimeMillis() - startTime
|
|
3111
|
+
Bridge.log("G1: BMP display completed in " + totalTime + "ms")
|
|
3112
|
+
|
|
3113
|
+
if (callback != null) {
|
|
3114
|
+
callback.onSuccess("both")
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
} catch (e: Exception) {
|
|
3118
|
+
Log.e(TAG, "Error in displayBitmapImageOptimized: " + e.message)
|
|
3119
|
+
if (callback != null)
|
|
3120
|
+
callback.onError("both", "Exception: " + e.message)
|
|
3121
|
+
} finally {
|
|
3122
|
+
isSendingBitmap = false
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
// Legacy method for fallback
|
|
3127
|
+
private fun displayBitmapImageLegacy(bmpData: ByteArray?, callback: BmpProgressCallback?) {
|
|
3128
|
+
Bridge.log("G1: Starting LEGACY BMP display process")
|
|
3129
|
+
|
|
3130
|
+
try {
|
|
3131
|
+
if (bmpData == null || bmpData.isEmpty()) {
|
|
3132
|
+
Log.e(TAG, "Invalid BMP data provided")
|
|
3133
|
+
if (callback != null)
|
|
3134
|
+
callback.onError("both", "Invalid BMP data")
|
|
3135
|
+
return
|
|
3136
|
+
}
|
|
3137
|
+
Bridge.log("G1: Processing BMP data, size: " + bmpData.size + " bytes")
|
|
3138
|
+
// Split into chunks and send
|
|
3139
|
+
val chunks = createBmpChunks(bmpData)
|
|
3140
|
+
Bridge.log("G1: Created " + chunks.size + " chunks")
|
|
3141
|
+
|
|
3142
|
+
// Send all chunks
|
|
3143
|
+
sendBmpChunks(chunks)
|
|
3144
|
+
// Send all chunks with progress
|
|
3145
|
+
val chunksSuccess = sendBmpChunksWithProgress(chunks, callback)
|
|
3146
|
+
if (!chunksSuccess) {
|
|
3147
|
+
Log.e(TAG, "Failed to send BMP chunks")
|
|
3148
|
+
if (callback != null)
|
|
3149
|
+
callback.onError("both", "Failed to send chunks")
|
|
3150
|
+
return
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
// Send end command with retry
|
|
3154
|
+
val endSuccess = sendBmpEndCommandWithRetry()
|
|
3155
|
+
if (!endSuccess) {
|
|
3156
|
+
Log.e(TAG, "Failed to send BMP end command")
|
|
3157
|
+
if (callback != null)
|
|
3158
|
+
callback.onError("both", "Failed to send end command")
|
|
3159
|
+
return
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
// Calculate and send CRC with proper algorithm
|
|
3163
|
+
val crcSuccess = sendBmpCrcWithRetry(bmpData)
|
|
3164
|
+
if (!crcSuccess) {
|
|
3165
|
+
Log.e(TAG, "Failed to send BMP CRC")
|
|
3166
|
+
if (callback != null)
|
|
3167
|
+
callback.onError("both", "CRC check failed")
|
|
3168
|
+
return
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
lastThingDisplayedWasAnImage = true
|
|
3172
|
+
|
|
3173
|
+
if (callback != null) {
|
|
3174
|
+
callback.onSuccess("both")
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
Bridge.log("G1: BMP display process completed successfully")
|
|
3178
|
+
|
|
3179
|
+
} catch (e: Exception) {
|
|
3180
|
+
Log.e(TAG, "Error in displayBitmapImage: " + e.message)
|
|
3181
|
+
if (callback != null)
|
|
3182
|
+
callback.onError("both", "Exception: " + e.message)
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
private fun createBmpChunks(bmpData: ByteArray): List<ByteArray> {
|
|
3187
|
+
val chunks = ArrayList<ByteArray>()
|
|
3188
|
+
val totalChunks = Math.ceil(bmpData.size.toDouble() / BMP_CHUNK_SIZE).toInt()
|
|
3189
|
+
Bridge.log("G1: Creating " + totalChunks + " chunks from " + bmpData.size + " bytes")
|
|
3190
|
+
|
|
3191
|
+
for (i in 0 until totalChunks) {
|
|
3192
|
+
val start = i * BMP_CHUNK_SIZE
|
|
3193
|
+
val end = Math.min(start + BMP_CHUNK_SIZE, bmpData.size)
|
|
3194
|
+
val chunk = Arrays.copyOfRange(bmpData, start, end)
|
|
3195
|
+
|
|
3196
|
+
// First chunk needs address bytes
|
|
3197
|
+
if (i == 0) {
|
|
3198
|
+
val headerWithAddress = ByteArray(2 + GLASSES_ADDRESS.size + chunk.size)
|
|
3199
|
+
headerWithAddress[0] = 0x15 // Command
|
|
3200
|
+
headerWithAddress[1] = (i and 0xFF).toByte() // Sequence
|
|
3201
|
+
System.arraycopy(GLASSES_ADDRESS, 0, headerWithAddress, 2, GLASSES_ADDRESS.size)
|
|
3202
|
+
System.arraycopy(chunk, 0, headerWithAddress, 6, chunk.size)
|
|
3203
|
+
chunks.add(headerWithAddress)
|
|
3204
|
+
} else {
|
|
3205
|
+
val header = ByteArray(2 + chunk.size)
|
|
3206
|
+
header[0] = 0x15 // Command
|
|
3207
|
+
header[1] = (i and 0xFF).toByte() // Sequence
|
|
3208
|
+
System.arraycopy(chunk, 0, header, 2, chunk.size)
|
|
3209
|
+
chunks.add(header)
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
return chunks
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
private fun sendBmpChunksWithProgress(chunks: List<ByteArray>, callback: BmpProgressCallback?): Boolean {
|
|
3216
|
+
if (updatingScreen)
|
|
3217
|
+
return false
|
|
3218
|
+
|
|
3219
|
+
for (i in 0 until chunks.size) {
|
|
3220
|
+
val chunk = chunks[i]
|
|
3221
|
+
Bridge.log("G1: Sending chunk " + i + " of " + chunks.size + ", size: " + chunk.size)
|
|
3222
|
+
|
|
3223
|
+
val success = sendDataSequentiallyWithTimeout(chunk, CHUNK_SEND_TIMEOUT_MS)
|
|
3224
|
+
if (!success) {
|
|
3225
|
+
Log.e(TAG, "Failed to send chunk " + i)
|
|
3226
|
+
return false
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// Report progress
|
|
3230
|
+
if (callback != null) {
|
|
3231
|
+
val offset = i * BMP_CHUNK_SIZE
|
|
3232
|
+
callback.onProgress("both", offset, i, chunks.size * BMP_CHUNK_SIZE)
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
// Platform-specific delay between chunks (matching Flutter implementation)
|
|
3236
|
+
try {
|
|
3237
|
+
Thread.sleep(ANDROID_CHUNK_DELAY_MS)
|
|
3238
|
+
} catch (e: InterruptedException) {
|
|
3239
|
+
Log.e(TAG, "Sleep interrupted: " + e.message)
|
|
3240
|
+
return false
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
return true
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// Optimized chunk sending - mimics iOS approach
|
|
3247
|
+
private fun sendBmpChunksOptimized(chunks: List<ByteArray>, callback: BmpProgressCallback?): Boolean {
|
|
3248
|
+
if (updatingScreen)
|
|
3249
|
+
return false
|
|
3250
|
+
|
|
3251
|
+
Bridge.log("G1: Sending " + chunks.size + " chunks using OPTIMIZED method")
|
|
3252
|
+
|
|
3253
|
+
// Send all chunks except last without waiting for response
|
|
3254
|
+
for (i in 0 until chunks.size - 1) {
|
|
3255
|
+
val chunk = chunks[i]
|
|
3256
|
+
val chunkIndex = i
|
|
3257
|
+
|
|
3258
|
+
if (USE_PARALLEL_BITMAP_WRITES && bitmapExecutor != null) {
|
|
3259
|
+
// Send to both glasses in parallel using executor
|
|
3260
|
+
val latch = CountDownLatch(2)
|
|
3261
|
+
|
|
3262
|
+
// Send to left glass
|
|
3263
|
+
bitmapExecutor!!.execute {
|
|
3264
|
+
try {
|
|
3265
|
+
if (leftGlassGatt != null && leftTxChar != null && isLeftConnected) {
|
|
3266
|
+
synchronized(leftTxChar!!) {
|
|
3267
|
+
leftTxChar!!.value = chunk
|
|
3268
|
+
leftTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
3269
|
+
leftGlassGatt!!.writeCharacteristic(leftTxChar)
|
|
3270
|
+
leftTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
} finally {
|
|
3274
|
+
latch.countDown()
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// Send to right glass
|
|
3279
|
+
bitmapExecutor!!.execute {
|
|
3280
|
+
try {
|
|
3281
|
+
if (rightGlassGatt != null && rightTxChar != null && isRightConnected) {
|
|
3282
|
+
synchronized(rightTxChar!!) {
|
|
3283
|
+
rightTxChar!!.value = chunk
|
|
3284
|
+
rightTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
3285
|
+
rightGlassGatt!!.writeCharacteristic(rightTxChar)
|
|
3286
|
+
rightTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
} finally {
|
|
3290
|
+
latch.countDown()
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
// Wait for both to complete (but don't wait for BLE response)
|
|
3295
|
+
try {
|
|
3296
|
+
latch.await(50, TimeUnit.MILLISECONDS) // Short timeout
|
|
3297
|
+
} catch (e: InterruptedException) {
|
|
3298
|
+
// Continue anyway
|
|
3299
|
+
}
|
|
3300
|
+
} else {
|
|
3301
|
+
// Fall back to sequential but still no wait
|
|
3302
|
+
sendBitmapChunkNoWait(chunk, true, true)
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
// Report progress
|
|
3306
|
+
if (callback != null) {
|
|
3307
|
+
val offset = chunkIndex * BMP_CHUNK_SIZE
|
|
3308
|
+
callback.onProgress("both", offset, chunkIndex, chunks.size * BMP_CHUNK_SIZE)
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
// Small delay between chunks (iOS uses 8ms)
|
|
3312
|
+
try {
|
|
3313
|
+
Thread.sleep(ANDROID_CHUNK_DELAY_MS)
|
|
3314
|
+
} catch (e: InterruptedException) {
|
|
3315
|
+
Log.e(TAG, "Sleep interrupted: " + e.message)
|
|
3316
|
+
return false
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
// Last chunk - wait for confirmation (like iOS)
|
|
3321
|
+
if (!chunks.isEmpty()) {
|
|
3322
|
+
val lastChunk = chunks[chunks.size - 1]
|
|
3323
|
+
Bridge.log("G1: Sending last chunk with confirmation")
|
|
3324
|
+
|
|
3325
|
+
val success = sendDataSequentiallyWithTimeout(lastChunk, 2000) // Shorter timeout for last chunk
|
|
3326
|
+
if (!success) {
|
|
3327
|
+
Log.e(TAG, "Failed to send last chunk")
|
|
3328
|
+
return false
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
if (callback != null) {
|
|
3332
|
+
val offset = (chunks.size - 1) * BMP_CHUNK_SIZE
|
|
3333
|
+
callback.onProgress("both", offset, chunks.size - 1, chunks.size * BMP_CHUNK_SIZE)
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
return true
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// Optimized end command sending
|
|
3341
|
+
private fun sendBmpEndCommandOptimized(): Boolean {
|
|
3342
|
+
Bridge.log("G1: Sending BMP end command (optimized)")
|
|
3343
|
+
|
|
3344
|
+
// End command needs confirmation, but with shorter timeout
|
|
3345
|
+
val success = sendDataSequentiallyWithTimeout(END_COMMAND, 1000)
|
|
3346
|
+
if (success) {
|
|
3347
|
+
Bridge.log("G1: BMP end command sent successfully")
|
|
3348
|
+
|
|
3349
|
+
// Small delay after end command
|
|
3350
|
+
try {
|
|
3351
|
+
Thread.sleep(10)
|
|
3352
|
+
} catch (e: InterruptedException) {
|
|
3353
|
+
// Ignore
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
return success
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
// Optimized CRC sending
|
|
3360
|
+
private fun sendBmpCrcOptimized(bmpData: ByteArray): Boolean {
|
|
3361
|
+
// Create data with address for CRC calculation
|
|
3362
|
+
val dataWithAddress = ByteArray(GLASSES_ADDRESS.size + bmpData.size)
|
|
3363
|
+
System.arraycopy(GLASSES_ADDRESS, 0, dataWithAddress, 0, GLASSES_ADDRESS.size)
|
|
3364
|
+
System.arraycopy(bmpData, 0, dataWithAddress, GLASSES_ADDRESS.size, bmpData.size)
|
|
3365
|
+
|
|
3366
|
+
// Calculate CRC32
|
|
3367
|
+
val crc = CRC32()
|
|
3368
|
+
crc.update(dataWithAddress)
|
|
3369
|
+
val crcValue = crc.value
|
|
3370
|
+
|
|
3371
|
+
// Create CRC command packet
|
|
3372
|
+
val crcCommand = ByteArray(5)
|
|
3373
|
+
crcCommand[0] = 0x16 // CRC command
|
|
3374
|
+
crcCommand[1] = ((crcValue shr 24) and 0xFF).toByte()
|
|
3375
|
+
crcCommand[2] = ((crcValue shr 16) and 0xFF).toByte()
|
|
3376
|
+
crcCommand[3] = ((crcValue shr 8) and 0xFF).toByte()
|
|
3377
|
+
crcCommand[4] = (crcValue and 0xFF).toByte()
|
|
3378
|
+
|
|
3379
|
+
Bridge.log("G1: Sending CRC command (optimized), CRC value: " + java.lang.Long.toHexString(crcValue))
|
|
3380
|
+
|
|
3381
|
+
// CRC needs confirmation, but with shorter timeout
|
|
3382
|
+
val success = sendDataSequentiallyWithTimeout(crcCommand, 1000)
|
|
3383
|
+
if (success) {
|
|
3384
|
+
Bridge.log("G1: CRC command sent successfully")
|
|
3385
|
+
}
|
|
3386
|
+
return success
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
private fun sendDataSequentiallyWithTimeout(data: ByteArray, timeoutMs: Long): Boolean {
|
|
3390
|
+
// Create a future to track the send operation
|
|
3391
|
+
val success = booleanArrayOf(false)
|
|
3392
|
+
val latch = CountDownLatch(1)
|
|
3393
|
+
|
|
3394
|
+
// Send the data asynchronously
|
|
3395
|
+
Thread {
|
|
3396
|
+
try {
|
|
3397
|
+
sendDataSequentially(data)
|
|
3398
|
+
success[0] = true
|
|
3399
|
+
} catch (e: Exception) {
|
|
3400
|
+
Log.e(TAG, "Error sending data: " + e.message)
|
|
3401
|
+
success[0] = false
|
|
3402
|
+
} finally {
|
|
3403
|
+
latch.countDown()
|
|
3404
|
+
}
|
|
3405
|
+
}.start()
|
|
3406
|
+
|
|
3407
|
+
// Wait for completion or timeout
|
|
3408
|
+
try {
|
|
3409
|
+
val completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS)
|
|
3410
|
+
return completed && success[0]
|
|
3411
|
+
} catch (e: InterruptedException) {
|
|
3412
|
+
Log.e(TAG, "Timeout waiting for data send")
|
|
3413
|
+
return false
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
private fun sendBmpEndCommandWithRetry(): Boolean {
|
|
3418
|
+
if (updatingScreen)
|
|
3419
|
+
return false
|
|
3420
|
+
|
|
3421
|
+
for (attempt in 0 until MAX_BMP_RETRY_ATTEMPTS) {
|
|
3422
|
+
Bridge.log("G1: Sending BMP end command, attempt " + (attempt + 1))
|
|
3423
|
+
|
|
3424
|
+
val success = sendDataSequentiallyWithTimeout(END_COMMAND, END_COMMAND_TIMEOUT_MS)
|
|
3425
|
+
if (success) {
|
|
3426
|
+
Bridge.log("G1: BMP end command sent successfully")
|
|
3427
|
+
return true
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
Log.w(TAG, "BMP end command failed, attempt " + (attempt + 1))
|
|
3431
|
+
|
|
3432
|
+
// Wait before retry
|
|
3433
|
+
try {
|
|
3434
|
+
Thread.sleep(BMP_RETRY_DELAY_MS)
|
|
3435
|
+
} catch (e: InterruptedException) {
|
|
3436
|
+
Log.e(TAG, "Sleep interrupted during retry")
|
|
3437
|
+
return false
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
Log.e(TAG, "Failed to send BMP end command after " + MAX_BMP_RETRY_ATTEMPTS + " attempts")
|
|
3442
|
+
return false
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
private fun sendBmpCrcWithRetry(bmpData: ByteArray): Boolean {
|
|
3446
|
+
// Create data with address for CRC calculation
|
|
3447
|
+
val dataWithAddress = ByteArray(GLASSES_ADDRESS.size + bmpData.size)
|
|
3448
|
+
System.arraycopy(GLASSES_ADDRESS, 0, dataWithAddress, 0, GLASSES_ADDRESS.size)
|
|
3449
|
+
System.arraycopy(bmpData, 0, dataWithAddress, GLASSES_ADDRESS.size, bmpData.size)
|
|
3450
|
+
|
|
3451
|
+
// Calculate CRC32-XZ (using standard CRC32 for now, but should be Crc32Xz)
|
|
3452
|
+
val crc = CRC32()
|
|
3453
|
+
crc.update(dataWithAddress)
|
|
3454
|
+
val crcValue = crc.value
|
|
3455
|
+
|
|
3456
|
+
// Create CRC command packet
|
|
3457
|
+
val crcCommand = ByteArray(5)
|
|
3458
|
+
crcCommand[0] = 0x16 // CRC command
|
|
3459
|
+
crcCommand[1] = ((crcValue shr 24) and 0xFF).toByte()
|
|
3460
|
+
crcCommand[2] = ((crcValue shr 16) and 0xFF).toByte()
|
|
3461
|
+
crcCommand[3] = ((crcValue shr 8) and 0xFF).toByte()
|
|
3462
|
+
crcCommand[4] = (crcValue and 0xFF).toByte()
|
|
3463
|
+
|
|
3464
|
+
Bridge.log("G1: Sending CRC command, CRC value: " + java.lang.Long.toHexString(crcValue))
|
|
3465
|
+
|
|
3466
|
+
// Send CRC with retry
|
|
3467
|
+
for (attempt in 0 until MAX_BMP_RETRY_ATTEMPTS) {
|
|
3468
|
+
val success = sendDataSequentiallyWithTimeout(crcCommand, CRC_COMMAND_TIMEOUT_MS)
|
|
3469
|
+
if (success) {
|
|
3470
|
+
Bridge.log("G1: CRC command sent successfully")
|
|
3471
|
+
return true
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
Log.w(TAG, "CRC command failed, attempt " + (attempt + 1))
|
|
3475
|
+
|
|
3476
|
+
try {
|
|
3477
|
+
Thread.sleep(BMP_RETRY_DELAY_MS)
|
|
3478
|
+
} catch (e: InterruptedException) {
|
|
3479
|
+
Log.e(TAG, "Sleep interrupted during CRC retry")
|
|
3480
|
+
return false
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
Log.e(TAG, "Failed to send CRC command after " + MAX_BMP_RETRY_ATTEMPTS + " attempts")
|
|
3485
|
+
return false
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
// Legacy methods for backward compatibility
|
|
3489
|
+
private fun sendBmpChunks(chunks: List<ByteArray>) {
|
|
3490
|
+
sendBmpChunksWithProgress(chunks, null)
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
private fun sendBmpEndCommand() {
|
|
3494
|
+
sendBmpEndCommandWithRetry()
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
private fun sendBmpCRC(bmpData: ByteArray) {
|
|
3498
|
+
sendBmpCrcWithRetry(bmpData)
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
private fun sendBmpToSide(bmpData: ByteArray, side: String) {
|
|
3502
|
+
// For now, send to both sides but could be modified for side-specific sending
|
|
3503
|
+
displayBitmapImage(bmpData, object : BmpProgressCallback {
|
|
3504
|
+
override fun onProgress(side: String, offset: Int, chunkIndex: Int, totalSize: Int) {
|
|
3505
|
+
Bridge.log("G1: BMP progress for " + side + ": " + offset + "/" + totalSize)
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
override fun onSuccess(side: String) {
|
|
3509
|
+
Bridge.log("G1: BMP sent successfully to " + side)
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
override fun onError(side: String, error: String) {
|
|
3513
|
+
Log.e(TAG, "BMP error for " + side + ": " + error)
|
|
3514
|
+
}
|
|
3515
|
+
})
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
private fun loadBmpFromFile(filePath: String): ByteArray? {
|
|
3519
|
+
try {
|
|
3520
|
+
val file = java.io.File(filePath)
|
|
3521
|
+
if (!file.exists()) {
|
|
3522
|
+
Log.e(TAG, "BMP file does not exist: " + filePath)
|
|
3523
|
+
return null
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
// Read file into byte array
|
|
3527
|
+
val fileData = ByteArray(file.length().toInt())
|
|
3528
|
+
java.io.FileInputStream(file).use { fis ->
|
|
3529
|
+
fis.read(fileData)
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
// Convert to 1-bit BMP format if needed
|
|
3533
|
+
return convertTo1BitBmp(fileData)
|
|
3534
|
+
|
|
3535
|
+
} catch (e: Exception) {
|
|
3536
|
+
Log.e(TAG, "Error loading BMP file: " + e.message)
|
|
3537
|
+
return null
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
private fun convertTo1BitBmp(originalBmp: ByteArray): ByteArray {
|
|
3542
|
+
// This is a placeholder - in a real implementation, you'd convert the BMP to
|
|
3543
|
+
// 1-bit format
|
|
3544
|
+
// For now, we'll assume the input is already in the correct format
|
|
3545
|
+
return originalBmp
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
private fun loadEmptyBmpFromAssets(): ByteArray? {
|
|
3549
|
+
try {
|
|
3550
|
+
context!!.assets.open("empty_bmp.bmp").use { iss ->
|
|
3551
|
+
return iss.readAllBytes()
|
|
3552
|
+
}
|
|
3553
|
+
} catch (e: IOException) {
|
|
3554
|
+
Log.e(TAG, "Failed to load BMP from assets: " + e.message)
|
|
3555
|
+
return null
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
fun clearBmpDisplay() {
|
|
3560
|
+
if (updatingScreen)
|
|
3561
|
+
return
|
|
3562
|
+
Bridge.log("G1: Clearing BMP display with EXIT command")
|
|
3563
|
+
val exitCommand = byteArrayOf(0x18)
|
|
3564
|
+
sendDataSequentially(exitCommand)
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
fun exitBmp() {
|
|
3568
|
+
clearBmpDisplay()
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
// Enhanced error handling and validation
|
|
3572
|
+
fun validateBmpFormat(bmpData: ByteArray?): Boolean {
|
|
3573
|
+
if (bmpData == null || bmpData.size < 54) { // Minimum BMP header size
|
|
3574
|
+
Log.e(TAG, "Invalid BMP data: null or too short")
|
|
3575
|
+
return false
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
// Check BMP signature
|
|
3579
|
+
if (bmpData[0] != 'B'.code.toByte() || bmpData[1] != 'M'.code.toByte()) {
|
|
3580
|
+
Log.e(TAG, "Invalid BMP signature")
|
|
3581
|
+
return false
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// Check if it's 1-bit format (simplified check)
|
|
3585
|
+
val bitsPerPixel = bmpData[28].toInt() and 0xFF
|
|
3586
|
+
if (bitsPerPixel != 1) {
|
|
3587
|
+
Log.w(TAG, "BMP is not 1-bit format (bits per pixel: " + bitsPerPixel + ")")
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
return true
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
// Batch processing with progress
|
|
3594
|
+
fun sendBmpBatch(bmpPaths: List<String>, callback: BmpProgressCallback?) {
|
|
3595
|
+
for (i in 0 until bmpPaths.size) {
|
|
3596
|
+
val path = bmpPaths[i]
|
|
3597
|
+
|
|
3598
|
+
if (callback != null) {
|
|
3599
|
+
callback.onProgress("batch", i, i, bmpPaths.size)
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
try {
|
|
3603
|
+
val bmpData = loadBmpFromFile(path)
|
|
3604
|
+
if (bmpData != null && validateBmpFormat(bmpData)) {
|
|
3605
|
+
displayBitmapImage(bmpData, object : BmpProgressCallback {
|
|
3606
|
+
override fun onProgress(side: String, offset: Int, chunkIndex: Int, totalSize: Int) {
|
|
3607
|
+
// Individual image progress
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
override fun onSuccess(side: String) {
|
|
3611
|
+
Bridge.log("G1: Successfully sent BMP: " + path)
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
override fun onError(side: String, error: String) {
|
|
3615
|
+
Log.e(TAG, "Failed to send BMP " + path + ": " + error)
|
|
3616
|
+
if (callback != null) {
|
|
3617
|
+
callback.onError("batch", "Failed to send " + path + ": " + error)
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
})
|
|
3621
|
+
|
|
3622
|
+
// Delay between images
|
|
3623
|
+
Thread.sleep(2000)
|
|
3624
|
+
} else {
|
|
3625
|
+
Log.e(TAG, "Invalid BMP format: " + path)
|
|
3626
|
+
if (callback != null) {
|
|
3627
|
+
callback.onError("batch", "Invalid BMP format: " + path)
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
} catch (e: Exception) {
|
|
3631
|
+
Log.e(TAG, "Error processing BMP " + path + ": " + e.message)
|
|
3632
|
+
if (callback != null) {
|
|
3633
|
+
callback.onError("batch", "Error processing " + path + ": " + e.message)
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
if (callback != null) {
|
|
3639
|
+
callback.onSuccess("batch")
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
private fun sendLoremIpsum() {
|
|
3644
|
+
if (updatingScreen)
|
|
3645
|
+
return
|
|
3646
|
+
val text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. "
|
|
3647
|
+
sendDataSequentially(createTextWallChunks(text))
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
// Enhanced clearing with state management
|
|
3651
|
+
fun clearBmpDisplayWithState() {
|
|
3652
|
+
if (updatingScreen) {
|
|
3653
|
+
Bridge.log("G1: Screen update in progress, queuing clear command")
|
|
3654
|
+
// Queue the clear command for later
|
|
3655
|
+
handler.postDelayed(this::clearBmpDisplay, 1000L)
|
|
3656
|
+
return
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
Bridge.log("G1: Clearing BMP display with EXIT command")
|
|
3660
|
+
updatingScreen = true
|
|
3661
|
+
|
|
3662
|
+
try {
|
|
3663
|
+
val exitCommand = byteArrayOf(0x18)
|
|
3664
|
+
val success = sendDataSequentiallyWithTimeout(exitCommand, 2000)
|
|
3665
|
+
if (success) {
|
|
3666
|
+
lastThingDisplayedWasAnImage = false
|
|
3667
|
+
Bridge.log("G1: BMP display cleared successfully")
|
|
3668
|
+
} else {
|
|
3669
|
+
Log.e(TAG, "Failed to clear BMP display")
|
|
3670
|
+
}
|
|
3671
|
+
} finally {
|
|
3672
|
+
updatingScreen = false
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// Optimized batch clearing
|
|
3677
|
+
fun clearAndDisplayBmp(bmpData: ByteArray?, callback: BmpProgressCallback?) {
|
|
3678
|
+
if (updatingScreen) {
|
|
3679
|
+
Bridge.log("G1: Screen update in progress, cannot clear and display")
|
|
3680
|
+
if (callback != null)
|
|
3681
|
+
callback.onError("both", "Screen update in progress")
|
|
3682
|
+
return
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
// Clear first, then display
|
|
3686
|
+
clearBmpDisplayWithState()
|
|
3687
|
+
|
|
3688
|
+
// Small delay to ensure clear completes
|
|
3689
|
+
handler.postDelayed({
|
|
3690
|
+
displayBitmapImage(bmpData, callback)
|
|
3691
|
+
}, 500L)
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// Efficient multiple image display with clearing
|
|
3695
|
+
fun displayBmpSequence(bmpDataList: List<ByteArray>?, callback: BmpProgressCallback?) {
|
|
3696
|
+
if (bmpDataList == null || bmpDataList.isEmpty()) {
|
|
3697
|
+
Log.e(TAG, "No BMP data provided for sequence")
|
|
3698
|
+
if (callback != null)
|
|
3699
|
+
callback.onError("sequence", "No BMP data provided")
|
|
3700
|
+
return
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
displayBmpSequenceInternal(bmpDataList, 0, callback)
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
private fun displayBmpSequenceInternal(bmpDataList: List<ByteArray>, index: Int, callback: BmpProgressCallback?) {
|
|
3707
|
+
if (index >= bmpDataList.size) {
|
|
3708
|
+
Bridge.log("G1: BMP sequence completed")
|
|
3709
|
+
if (callback != null)
|
|
3710
|
+
callback.onSuccess("sequence")
|
|
3711
|
+
return
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
val bmpData = bmpDataList[index]
|
|
3715
|
+
displayBitmapImage(bmpData, object : BmpProgressCallback {
|
|
3716
|
+
override fun onProgress(side: String, offset: Int, chunkIndex: Int, totalSize: Int) {
|
|
3717
|
+
if (callback != null) {
|
|
3718
|
+
// Calculate overall progress including sequence position
|
|
3719
|
+
val overallProgress = (index * 100) / bmpDataList.size
|
|
3720
|
+
callback.onProgress("sequence", overallProgress, index, bmpDataList.size)
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
override fun onSuccess(side: String) {
|
|
3725
|
+
Bridge.log("G1: BMP " + (index + 1) + " of " + bmpDataList.size + " displayed successfully")
|
|
3726
|
+
|
|
3727
|
+
// Schedule next image with delay
|
|
3728
|
+
handler.postDelayed({
|
|
3729
|
+
displayBmpSequenceInternal(bmpDataList, index + 1, callback)
|
|
3730
|
+
}, 2000L) // 2 second delay between images
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
override fun onError(side: String, error: String) {
|
|
3734
|
+
Log.e(TAG, "Failed to display BMP " + (index + 1) + ": " + error)
|
|
3735
|
+
if (callback != null) {
|
|
3736
|
+
callback.onError("sequence", "Failed to display BMP " + (index + 1) + ": " + error)
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
})
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
private fun quickRestartG1() {
|
|
3743
|
+
Bridge.log("G1: Sending restart 0x23 0x72 Command")
|
|
3744
|
+
sendDataSequentially(byteArrayOf(0x23.toByte(), 0x72.toByte())) // quick restart comand
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
override fun setMicEnabled(enabled: Boolean) {
|
|
3748
|
+
Bridge.log("G1: setMicEnabled(): " + enabled)
|
|
3749
|
+
|
|
3750
|
+
// Update the shouldUseGlassesMic flag to reflect the current state
|
|
3751
|
+
this.shouldUseGlassesMic = enabled
|
|
3752
|
+
Bridge.log("G1: Updated shouldUseGlassesMic to: " + shouldUseGlassesMic)
|
|
3753
|
+
|
|
3754
|
+
if (enabled) {
|
|
3755
|
+
Bridge.log("G1: Microphone enabled, starting audio input handling")
|
|
3756
|
+
sendSetMicEnabled(true, 10)
|
|
3757
|
+
startMicBeat(MICBEAT_INTERVAL_MS.toInt())
|
|
3758
|
+
} else {
|
|
3759
|
+
Bridge.log("G1: Microphone disabled, stopping audio input handling")
|
|
3760
|
+
sendSetMicEnabled(false, 10)
|
|
3761
|
+
stopMicBeat()
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
override fun sortMicRanking(list: MutableList<String>): MutableList<String> {
|
|
3766
|
+
return list
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
/**
|
|
3770
|
+
* Emits serial number information to React Native (matching iOS implementation)
|
|
3771
|
+
*/
|
|
3772
|
+
private fun emitSerialNumberInfo(serialNumber: String, style: String, color: String) {
|
|
3773
|
+
try {
|
|
3774
|
+
val eventBody = JSONObject()
|
|
3775
|
+
eventBody.put("serialNumber", serialNumber)
|
|
3776
|
+
eventBody.put("style", style)
|
|
3777
|
+
eventBody.put("color", color)
|
|
3778
|
+
|
|
3779
|
+
// Bridge.sendTypedMessage("glasses_serial_number", eventBody);
|
|
3780
|
+
Bridge.log("G1: 📱 Emitted serial number info: " + serialNumber + ", Style: " + style + ", Color: " + color)
|
|
3781
|
+
DeviceStore.apply("glasses", "serialNumber", serialNumber)
|
|
3782
|
+
DeviceStore.apply("glasses", "style", style)
|
|
3783
|
+
DeviceStore.apply("glasses", "color", color)
|
|
3784
|
+
|
|
3785
|
+
} catch (e: Exception) {
|
|
3786
|
+
Bridge.log("G1: Error emitting serial number info: " + e.message)
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
/**
|
|
3791
|
+
* Decodes serial number from manufacturer data bytes
|
|
3792
|
+
*
|
|
3793
|
+
* @param manufacturerData The manufacturer data bytes
|
|
3794
|
+
* @return Decoded serial number string or null if not found
|
|
3795
|
+
*/
|
|
3796
|
+
private fun decodeSerialFromManufacturerData(manufacturerData: ByteArray?): String? {
|
|
3797
|
+
if (manufacturerData == null || manufacturerData.size < 10) {
|
|
3798
|
+
return null
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
try {
|
|
3802
|
+
// Convert hex bytes to ASCII string
|
|
3803
|
+
val serialBuilder = StringBuilder()
|
|
3804
|
+
for (i in 0 until manufacturerData.size) {
|
|
3805
|
+
val b = manufacturerData[i]
|
|
3806
|
+
if (b.toInt() == 0x00) {
|
|
3807
|
+
// Stop at null terminator
|
|
3808
|
+
break
|
|
3809
|
+
}
|
|
3810
|
+
if (b.toInt() >= 0x20 && b.toInt() <= 0x7E) {
|
|
3811
|
+
// Only include printable ASCII characters
|
|
3812
|
+
serialBuilder.append(b.toInt().toChar())
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
val decodedString = serialBuilder.toString().trim()
|
|
3817
|
+
|
|
3818
|
+
// Check if it looks like a valid Even G1 serial number
|
|
3819
|
+
if (decodedString.length >= 12 &&
|
|
3820
|
+
(decodedString.startsWith("S1") || decodedString.startsWith("100")
|
|
3821
|
+
|| decodedString.startsWith("110"))) {
|
|
3822
|
+
return decodedString
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
return null
|
|
3826
|
+
} catch (e: Exception) {
|
|
3827
|
+
Log.e(TAG, "Error decoding manufacturer data: " + e.message)
|
|
3828
|
+
return null
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
override fun cleanup() {
|
|
3833
|
+
// TODO:
|
|
3834
|
+
}
|
|
3835
|
+
}
|