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