@mentra/bluetooth-sdk 0.1.10 → 0.1.11
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 +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +39 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +22 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +829 -643
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +40 -64
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -1
- package/build/BluetoothSdk.types.d.ts +12 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +1 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +14 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +3 -0
- package/ios/Source/Bridge.swift +10 -0
- package/ios/Source/DeviceManager.swift +24 -21
- package/ios/Source/DeviceStore.swift +8 -2
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/sgcs/G1.swift +3 -3
- package/ios/Source/sgcs/G2.swift +1022 -640
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +2 -2
- package/ios/Source/sgcs/MentraNex.swift +215 -85
- package/ios/Source/sgcs/SGCManager.swift +13 -4
- package/ios/Source/sgcs/Simulated.swift +2 -2
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +13 -1
- package/src/_private/BluetoothSdkModule.ts +22 -3
- package/src/index.ts +3 -0
|
@@ -27,6 +27,13 @@ import java.io.ByteArrayOutputStream
|
|
|
27
27
|
import java.util.TimeZone
|
|
28
28
|
import java.util.UUID
|
|
29
29
|
import java.util.regex.Pattern
|
|
30
|
+
import kotlinx.coroutines.CoroutineScope
|
|
31
|
+
import kotlinx.coroutines.Dispatchers
|
|
32
|
+
import kotlinx.coroutines.SupervisorJob
|
|
33
|
+
import kotlinx.coroutines.delay
|
|
34
|
+
import kotlinx.coroutines.launch
|
|
35
|
+
import kotlinx.coroutines.sync.Mutex
|
|
36
|
+
import kotlinx.coroutines.sync.withLock
|
|
30
37
|
|
|
31
38
|
// ---------- G2 Protocol Constants ----------
|
|
32
39
|
|
|
@@ -49,6 +56,7 @@ private enum class ServiceID(val value: Byte) {
|
|
|
49
56
|
DASHBOARD(0x01), // UI_BACKGROUND_DASHBOARD_APP_ID
|
|
50
57
|
MENU(0x03), // UI_FOREGROUND_MEUN_ID (typo is intentional — matches Even's proto)
|
|
51
58
|
EVEN_AI(0x07), // UI_FOREGROUND_EVEN_AI_ID
|
|
59
|
+
NAVIGATION(0x08), // UI_BACKGROUND_NAVIGATION_ID (compass/heading lives here)
|
|
52
60
|
G2_SETTING(0x09), // UI_SETTING_APP_ID
|
|
53
61
|
GESTURE_CTRL(0x0D), // gesture_ctrl lifecycle signals
|
|
54
62
|
ONBOARDING(0x10), // UI_ONBOARDING_APP_ID
|
|
@@ -69,7 +77,21 @@ private enum class EvenHubCmd(val value: Int) {
|
|
|
69
77
|
REBUILD_PAGE(7),
|
|
70
78
|
SHUTDOWN_PAGE(9),
|
|
71
79
|
HEARTBEAT(12),
|
|
72
|
-
AUDIO_CONTROL(15)
|
|
80
|
+
AUDIO_CONTROL(15),
|
|
81
|
+
IMU_CONTROL(19) // APP_REQUEST_IMU_CTR_PACKET (confirmed via on-device brute-force)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Navigation_Cmd_list from navigation.proto (service 0x08)
|
|
85
|
+
private enum class NavigationCmd(val value: Int) {
|
|
86
|
+
APP_SEND_HEARTBEAT(0), // APP_SEND_HEARTBEAT_CMD
|
|
87
|
+
APP_REQUEST_START_UP(5), // APP_REQUEST_START_UP — begin navigation/compass session
|
|
88
|
+
APP_SEND_BASIC_INFO(7), // APP_SEND_BASIC_INFO
|
|
89
|
+
APP_REQUEST_EXIT(12), // APP_REQUEST_EXIT
|
|
90
|
+
OS_NOTIFY_EXIT(13), // OS_NOTIFY_EXIT
|
|
91
|
+
OS_NOTIFY_REVIEW_CHANGED(14), // OS_NOTIFY_REVIEW_CHANGED
|
|
92
|
+
OS_NOTIFY_COMPASS_CHANGED(15), // OS_NOTIFY_COMPASS_CHANGED — heading update
|
|
93
|
+
OS_NOTIFY_COMPASS_CALIBRATE_START(16), // OS_NOTIFY_COMPASS_CALIBRATE_STRAT (sic)
|
|
94
|
+
OS_NOTIFY_COMPASS_CALIBRATE_COMPLETE(17) // OS_NOTIFY_COMPASS_CALIBRATE_COMPLETE
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
// EvenHub response command IDs (glasses → phone)
|
|
@@ -86,7 +108,8 @@ private enum class OsEventType(val value: Int) {
|
|
|
86
108
|
FOREGROUND_ENTER(4),
|
|
87
109
|
FOREGROUND_EXIT(5),
|
|
88
110
|
ABNORMAL_EXIT(6),
|
|
89
|
-
SYSTEM_EXIT(7)
|
|
111
|
+
SYSTEM_EXIT(7),
|
|
112
|
+
IMU_DATA_REPORT(8); // IMU_DATA_REPORT — Sys_ItemEvent carries imuData
|
|
90
113
|
|
|
91
114
|
companion object {
|
|
92
115
|
fun fromInt(v: Int): OsEventType? = entries.find { it.value == v }
|
|
@@ -465,6 +488,52 @@ private object EvenHubProto {
|
|
|
465
488
|
val msg = audioCtrCmd(enable)
|
|
466
489
|
return evenHubMessage(EvenHubCmd.AUDIO_CONTROL, 18, msg, magicRandom = magicRandom)
|
|
467
490
|
}
|
|
491
|
+
|
|
492
|
+
// ---------- IMU control ----------
|
|
493
|
+
//
|
|
494
|
+
// Wire format recovered by on-device brute-force (sample magnitude ≈ 1.0 g confirms
|
|
495
|
+
// the decode). Shapes from even_hub_sdk@0.0.10; numeric proto tags confirmed live:
|
|
496
|
+
// EvenHub_Cmd_List IMU command = 19
|
|
497
|
+
// evenhub_main_msg_ctx ImuCtrlCmd slot = field 20
|
|
498
|
+
// ImuCtrlCmd { field 1 = IMU_ReportEn (bool), field 2 = reportFrq (pacing 100…1000) }
|
|
499
|
+
// Report path: cmd=2 (osNotifyEventToApp) → SendDeviceEvent.field13 →
|
|
500
|
+
// Sys_ItemEvent { field 1 = eventType = 8 (IMU_DATA_REPORT),
|
|
501
|
+
// field 3 = imuData = IMU_Report_Data }
|
|
502
|
+
// IMU_Report_Data { field 1 = x, 2 = y, 3 = z } — each a 32-bit float (NOT double),
|
|
503
|
+
// gravity-normalized (|v| ≈ 1 at rest).
|
|
504
|
+
const val IMU_CTRL_SUB_FIELD = 20
|
|
505
|
+
|
|
506
|
+
/** ImuReportPace pacing codes (protocol values, NOT literal Hz). Step 100, 100…1000. */
|
|
507
|
+
const val IMU_PACE_P100 = 100
|
|
508
|
+
const val IMU_PACE_P500 = 500
|
|
509
|
+
const val IMU_PACE_P1000 = 1000
|
|
510
|
+
|
|
511
|
+
/** Build an ImuCtrlCmd sub-message. */
|
|
512
|
+
fun imuCtrlCmd(enable: Boolean, reportFrq: Int): ByteArray {
|
|
513
|
+
val w = ProtobufWriter()
|
|
514
|
+
w.writeInt32Field(1, if (enable) 1 else 0) // IMU_ReportEn
|
|
515
|
+
if (enable) {
|
|
516
|
+
w.writeInt32Field(2, reportFrq) // reportFrq (pacing code 100…1000)
|
|
517
|
+
}
|
|
518
|
+
return w.toByteArray()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Build a full evenhub_main_msg_ctx that enables/disables IMU reporting. `reportFrq` is an
|
|
523
|
+
* ImuReportPace pacing code; ignored when disabling.
|
|
524
|
+
*/
|
|
525
|
+
fun imuControlMessage(
|
|
526
|
+
enable: Boolean,
|
|
527
|
+
reportFrq: Int = IMU_PACE_P100,
|
|
528
|
+
magicRandom: Int = 0
|
|
529
|
+
): ByteArray {
|
|
530
|
+
val imuMsg = imuCtrlCmd(enable, reportFrq)
|
|
531
|
+
val w = ProtobufWriter()
|
|
532
|
+
w.writeInt32Field(1, EvenHubCmd.IMU_CONTROL.value) // Cmd
|
|
533
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom
|
|
534
|
+
w.writeMessageField(IMU_CTRL_SUB_FIELD, imuMsg) // ImuCtrlCmd slot (field 20)
|
|
535
|
+
return w.toByteArray()
|
|
536
|
+
}
|
|
468
537
|
}
|
|
469
538
|
|
|
470
539
|
// ---------- DevSettings Auth Protobuf Builders ----------
|
|
@@ -667,11 +736,23 @@ private object OnboardingProto {
|
|
|
667
736
|
// ---------- EvenAI Protobuf Builders (even_ai.proto, service ID 7) ----------
|
|
668
737
|
|
|
669
738
|
private object EvenAIProto {
|
|
739
|
+
/**
|
|
740
|
+
* EvenAIDataPackage with CONFIG command to toggle Hey Even wakeword.
|
|
741
|
+
* voiceSwitch: 0 = OFF, 1 = ON.
|
|
742
|
+
*
|
|
743
|
+
* Wire format confirmed by sniffing the official app toggling the setting:
|
|
744
|
+
* EvenAIConfig (field 13) = { f1=voiceSwitch, f2=32 }
|
|
745
|
+
* The app OMITS f1 when disabling (proto3 zero) and sends f2=32 (0x20), NOT 80.
|
|
746
|
+
* Observed echoes: ON → 6A04 08 01 10 20 ({f1:1, f2:32})
|
|
747
|
+
* OFF → 6A02 10 20 ({f2:32})
|
|
748
|
+
*/
|
|
670
749
|
fun setHeyEven(magicRandom: Int, enabled: Boolean): ByteArray {
|
|
671
750
|
// EvenAIConfig
|
|
672
751
|
val configW = ProtobufWriter()
|
|
673
|
-
|
|
674
|
-
|
|
752
|
+
if (enabled) {
|
|
753
|
+
configW.writeInt32Field(1, 1) // voiceSwitch (omitted when off, matching the app)
|
|
754
|
+
}
|
|
755
|
+
configW.writeInt32Field(2, 32) // streamSpeed (always sent, app uses 32)
|
|
675
756
|
|
|
676
757
|
// EvenAIDataPackage
|
|
677
758
|
val w = ProtobufWriter()
|
|
@@ -1226,7 +1307,6 @@ class G2 : SGCManager() {
|
|
|
1226
1307
|
private val EVEN_HUB_QUEUE_TICK_MS = 100L
|
|
1227
1308
|
private var startupPageCreated: Boolean = false
|
|
1228
1309
|
private var pageCreated: Boolean = false
|
|
1229
|
-
private var pageHasTextContainer: Boolean = false
|
|
1230
1310
|
private var currentTextContent: String = ""
|
|
1231
1311
|
private var textContainerID: Int = 1
|
|
1232
1312
|
private var imageSessionCounter: Int = 0
|
|
@@ -1242,16 +1322,29 @@ class G2 : SGCManager() {
|
|
|
1242
1322
|
private var dashboardMenuItems: MutableList<MenuProto.MenuItem> = mutableListOf()
|
|
1243
1323
|
private var activeMenuAppId: Int? = null
|
|
1244
1324
|
private var lastClickTimestamp: Long? = null
|
|
1325
|
+
private var lastEvenHubResponseTimestamp: Long? = null
|
|
1245
1326
|
private var lastMenuSelectTimestamp: Long? = null
|
|
1246
1327
|
private var lastGestureCtrlTimestamp: Long? = null
|
|
1247
1328
|
|
|
1329
|
+
/**
|
|
1330
|
+
* Serializes all display mutations (text/bitmap/clear/rebuild). Each public display entry point
|
|
1331
|
+
* launches on [displayScope] and runs its body under [displayMutex] so page shutdown/create and
|
|
1332
|
+
* fragment sends can never interleave with another update on the glasses.
|
|
1333
|
+
*/
|
|
1334
|
+
private val displayScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
1335
|
+
private val displayMutex = Mutex()
|
|
1336
|
+
|
|
1248
1337
|
/** A tracked image container on the current page. Keyed by its rect for reuse. */
|
|
1249
1338
|
private data class ImgContainer(
|
|
1250
1339
|
val id: Int,
|
|
1251
1340
|
val x: Int,
|
|
1252
1341
|
val y: Int,
|
|
1253
1342
|
val width: Int,
|
|
1254
|
-
val height: Int
|
|
1343
|
+
val height: Int,
|
|
1344
|
+
// The converted BMP cached so the container can be re-sent on a page rebuild.
|
|
1345
|
+
// NOTE: this ByteArray makes the auto-generated equals/hashCode unreliable, but the
|
|
1346
|
+
// class is only ever compared via matches()/id, so that is never relied upon.
|
|
1347
|
+
var bmpData: ByteArray
|
|
1255
1348
|
) {
|
|
1256
1349
|
val name: String
|
|
1257
1350
|
get() = "img-$id"
|
|
@@ -1260,18 +1353,65 @@ class G2 : SGCManager() {
|
|
|
1260
1353
|
this.x == x && this.y == y && this.width == width && this.height == height
|
|
1261
1354
|
}
|
|
1262
1355
|
|
|
1356
|
+
/** A tracked text container on the current page. Keyed by its rect for reuse. */
|
|
1357
|
+
private data class TextContainer(
|
|
1358
|
+
val id: Int,
|
|
1359
|
+
val x: Int,
|
|
1360
|
+
val y: Int,
|
|
1361
|
+
val width: Int,
|
|
1362
|
+
val height: Int,
|
|
1363
|
+
var content: String,
|
|
1364
|
+
val borderWidth: Int,
|
|
1365
|
+
val borderColor: Int,
|
|
1366
|
+
val borderRadius: Int,
|
|
1367
|
+
val paddingLength: Int
|
|
1368
|
+
) {
|
|
1369
|
+
val name: String
|
|
1370
|
+
get() = "text-$id"
|
|
1371
|
+
|
|
1372
|
+
fun matches(
|
|
1373
|
+
x: Int,
|
|
1374
|
+
y: Int,
|
|
1375
|
+
width: Int,
|
|
1376
|
+
height: Int,
|
|
1377
|
+
borderWidth: Int,
|
|
1378
|
+
borderColor: Int,
|
|
1379
|
+
borderRadius: Int,
|
|
1380
|
+
paddingLength: Int
|
|
1381
|
+
): Boolean =
|
|
1382
|
+
this.x == x &&
|
|
1383
|
+
this.y == y &&
|
|
1384
|
+
this.width == width &&
|
|
1385
|
+
this.height == height &&
|
|
1386
|
+
this.borderWidth == borderWidth &&
|
|
1387
|
+
this.borderColor == borderColor &&
|
|
1388
|
+
this.borderRadius == borderRadius &&
|
|
1389
|
+
this.paddingLength == paddingLength
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1263
1392
|
/**
|
|
1264
1393
|
* Live list of image containers on the page, ordered oldest→newest (for LRU eviction). The page
|
|
1265
1394
|
* may hold at most 4 image containers (IDs from the pool below).
|
|
1266
1395
|
*/
|
|
1267
1396
|
private val imageContainers: MutableList<ImgContainer> = mutableListOf()
|
|
1397
|
+
private val textContainers: MutableList<TextContainer> = mutableListOf()
|
|
1268
1398
|
/** Fixed pool of container IDs the page protocol expects. */
|
|
1269
1399
|
private val imageContainerIDPool: List<Int> = listOf(10, 11, 12, 13)
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
private val
|
|
1273
|
-
private val
|
|
1400
|
+
private val textContainerIDPool: List<Int> = listOf(1, 2, 3, 4, 5, 6)
|
|
1401
|
+
/** Default container seeded into every fresh page: 200x100 centered at 188,44. */
|
|
1402
|
+
private val defaultImgX = 188
|
|
1403
|
+
private val defaultImgY = 44
|
|
1404
|
+
private val defaultImgWidth = 200
|
|
1274
1405
|
private val defaultImgHeight = 100
|
|
1406
|
+
/** Default text container: full-screen 576x288 with a 4px padding. */
|
|
1407
|
+
private val defaultTextX = 0
|
|
1408
|
+
private val defaultTextY = 0
|
|
1409
|
+
private val defaultTextWidth = 576
|
|
1410
|
+
private val defaultTextHeight = 288
|
|
1411
|
+
private val defaultTextBorderWidth = 0
|
|
1412
|
+
private val defaultTextBorderColor = 0
|
|
1413
|
+
private val defaultTextBorderRadius = 0
|
|
1414
|
+
private val defaultTextPaddingLength = 4
|
|
1275
1415
|
|
|
1276
1416
|
// Battery state
|
|
1277
1417
|
private var _batteryLevel: Int = -1
|
|
@@ -1329,6 +1469,7 @@ class G2 : SGCManager() {
|
|
|
1329
1469
|
left: Boolean = false,
|
|
1330
1470
|
right: Boolean = true
|
|
1331
1471
|
) {
|
|
1472
|
+
// Bridge.log("G2: sendToGlasses() - sending ${packets.size} packets first byte: ${packets[0][0]}")
|
|
1332
1473
|
if (packets.isEmpty()) return
|
|
1333
1474
|
// Single-packet sends (the common case for text/settings) go straight through.
|
|
1334
1475
|
if (packets.size == 1) {
|
|
@@ -1368,6 +1509,16 @@ class G2 : SGCManager() {
|
|
|
1368
1509
|
sendToGlasses(packets, left = left, right = right)
|
|
1369
1510
|
}
|
|
1370
1511
|
|
|
1512
|
+
private fun sendNavigationCommand(payload: ByteArray) {
|
|
1513
|
+
val packets =
|
|
1514
|
+
sendManager.buildPackets(
|
|
1515
|
+
serviceId = ServiceID.NAVIGATION.value,
|
|
1516
|
+
payload = payload,
|
|
1517
|
+
reserveFlag = true
|
|
1518
|
+
)
|
|
1519
|
+
sendToGlasses(packets)
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1371
1522
|
private fun sendG2SettingCommand(payload: ByteArray) {
|
|
1372
1523
|
val packets =
|
|
1373
1524
|
sendManager.buildPackets(
|
|
@@ -1440,7 +1591,7 @@ class G2 : SGCManager() {
|
|
|
1440
1591
|
|
|
1441
1592
|
// ---------- Authentication Sequence ----------
|
|
1442
1593
|
|
|
1443
|
-
private fun runAuthSequence() {
|
|
1594
|
+
private suspend fun runAuthSequence() {
|
|
1444
1595
|
Bridge.log("G2: Running auth sequence")
|
|
1445
1596
|
|
|
1446
1597
|
// Auth to left side
|
|
@@ -1450,443 +1601,113 @@ class G2 : SGCManager() {
|
|
|
1450
1601
|
}
|
|
1451
1602
|
|
|
1452
1603
|
// Small delay then auth right + pipe role change + time sync
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
val heyEvenOff =
|
|
1489
|
-
EvenAIProto.setHeyEven(
|
|
1490
|
-
sendManager
|
|
1491
|
-
.nextMagicRandom(),
|
|
1492
|
-
false
|
|
1493
|
-
)
|
|
1494
|
-
sendEvenAICommand(heyEvenOff)
|
|
1495
|
-
Bridge.log("G2: Disabled Hey Even wakeword")
|
|
1496
|
-
|
|
1497
|
-
// Replicate Even app's full init sequence
|
|
1498
|
-
// for menu selection support:
|
|
1499
|
-
|
|
1500
|
-
// 0. Universe settings (g2_setting cmd=1
|
|
1501
|
-
// field3 with field9=universe settings)
|
|
1502
|
-
val univW = ProtobufWriter()
|
|
1503
|
-
univW.writeInt32Field(
|
|
1504
|
-
1,
|
|
1505
|
-
1
|
|
1506
|
-
) // DeviceReceiveInfo
|
|
1507
|
-
univW.writeInt32Field(
|
|
1508
|
-
2,
|
|
1509
|
-
sendManager.nextMagicRandom()
|
|
1510
|
-
)
|
|
1511
|
-
univW.writeMessageField(
|
|
1512
|
-
3,
|
|
1513
|
-
byteArrayOf(
|
|
1514
|
-
0x4A,
|
|
1515
|
-
0x0A, // field 9, length 10
|
|
1516
|
-
0x08,
|
|
1517
|
-
0x00, // unitFormat=0
|
|
1518
|
-
0x10,
|
|
1519
|
-
0x00, // distanceUnit=0
|
|
1520
|
-
0x18,
|
|
1521
|
-
dashboardHalfDayFormat().toByte(),
|
|
1522
|
-
// timeFormat / halfDayFormat
|
|
1523
|
-
0x20,
|
|
1524
|
-
0x00, // dateFormat=0
|
|
1525
|
-
0x28,
|
|
1526
|
-
dashboardTemperatureUnit().toByte(),
|
|
1527
|
-
// temperatureUnit
|
|
1528
|
-
)
|
|
1529
|
-
)
|
|
1530
|
-
sendG2SettingCommand(univW.toByteArray())
|
|
1531
|
-
|
|
1532
|
-
// 1. gesture_ctrl init (field1=0,
|
|
1533
|
-
// field2=magicRandom)
|
|
1534
|
-
val gestureInitW = ProtobufWriter()
|
|
1535
|
-
gestureInitW.writeInt32Field(1, 0)
|
|
1536
|
-
gestureInitW.writeInt32Field(
|
|
1537
|
-
2,
|
|
1538
|
-
sendManager.nextMagicRandom()
|
|
1539
|
-
)
|
|
1540
|
-
sendGestureCtrlCommand(
|
|
1541
|
-
gestureInitW.toByteArray()
|
|
1542
|
-
)
|
|
1543
|
-
|
|
1544
|
-
// 2. ui_setting_app (0x0C) — query
|
|
1545
|
-
val uiSettW = ProtobufWriter()
|
|
1546
|
-
uiSettW.writeInt32Field(
|
|
1547
|
-
1,
|
|
1548
|
-
2
|
|
1549
|
-
) // cmd = DeviceReceiveRequest
|
|
1550
|
-
uiSettW.writeInt32Field(
|
|
1551
|
-
2,
|
|
1552
|
-
sendManager.nextMagicRandom()
|
|
1553
|
-
)
|
|
1554
|
-
uiSettW.writeMessageField(
|
|
1555
|
-
4,
|
|
1556
|
-
byteArrayOf(0x08, 0x01, 0x10, 0x00)
|
|
1557
|
-
) // {1:1, 2:0}
|
|
1558
|
-
sendToGlasses(
|
|
1559
|
-
sendManager.buildPackets(
|
|
1560
|
-
serviceId = 0x0C,
|
|
1561
|
-
payload =
|
|
1562
|
-
uiSettW.toByteArray(),
|
|
1563
|
-
reserveFlag = true
|
|
1564
|
-
)
|
|
1565
|
-
)
|
|
1566
|
-
|
|
1567
|
-
// 3. teleprompter (0x10) — config (cmd=1,
|
|
1568
|
-
// field3={1:4})
|
|
1569
|
-
val teleW = ProtobufWriter()
|
|
1570
|
-
teleW.writeInt32Field(1, 1)
|
|
1571
|
-
teleW.writeInt32Field(
|
|
1572
|
-
2,
|
|
1573
|
-
sendManager.nextMagicRandom()
|
|
1574
|
-
)
|
|
1575
|
-
teleW.writeMessageField(
|
|
1576
|
-
3,
|
|
1577
|
-
byteArrayOf(0x08, 0x04)
|
|
1578
|
-
) // {1:4}
|
|
1579
|
-
sendToGlasses(
|
|
1580
|
-
sendManager.buildPackets(
|
|
1581
|
-
serviceId = 0x10,
|
|
1582
|
-
payload =
|
|
1583
|
-
teleW.toByteArray(),
|
|
1584
|
-
reserveFlag = true
|
|
1585
|
-
)
|
|
1586
|
-
)
|
|
1587
|
-
|
|
1588
|
-
// 4. EvenHub CTRL on service 0x81 (cmd=1,
|
|
1589
|
-
// empty field3)
|
|
1590
|
-
val ehCtrlW = ProtobufWriter()
|
|
1591
|
-
ehCtrlW.writeInt32Field(1, 1)
|
|
1592
|
-
ehCtrlW.writeInt32Field(
|
|
1593
|
-
2,
|
|
1594
|
-
sendManager.nextMagicRandom()
|
|
1595
|
-
)
|
|
1596
|
-
ehCtrlW.writeMessageField(3, ByteArray(0))
|
|
1597
|
-
sendEvenHubCtrlCommand(
|
|
1598
|
-
ehCtrlW.toByteArray()
|
|
1599
|
-
)
|
|
1600
|
-
|
|
1601
|
-
// 5. calendar (0x04) — config
|
|
1602
|
-
val calW = ProtobufWriter()
|
|
1603
|
-
calW.writeInt32Field(1, 1)
|
|
1604
|
-
calW.writeInt32Field(
|
|
1605
|
-
2,
|
|
1606
|
-
sendManager.nextMagicRandom()
|
|
1607
|
-
)
|
|
1608
|
-
calW.writeMessageField(
|
|
1609
|
-
3,
|
|
1610
|
-
byteArrayOf(
|
|
1611
|
-
0x08,
|
|
1612
|
-
0x01,
|
|
1613
|
-
0x10,
|
|
1614
|
-
0x01,
|
|
1615
|
-
0x18,
|
|
1616
|
-
0x05,
|
|
1617
|
-
0x28,
|
|
1618
|
-
0x01
|
|
1619
|
-
)
|
|
1620
|
-
)
|
|
1621
|
-
sendToGlasses(
|
|
1622
|
-
sendManager.buildPackets(
|
|
1623
|
-
serviceId = 0x04,
|
|
1624
|
-
payload =
|
|
1625
|
-
calW.toByteArray(),
|
|
1626
|
-
reserveFlag = true
|
|
1627
|
-
)
|
|
1628
|
-
)
|
|
1629
|
-
|
|
1630
|
-
// 6. Dashboard init (0x01) — display
|
|
1631
|
-
// settings
|
|
1632
|
-
val dashDisplayW = ProtobufWriter()
|
|
1633
|
-
dashDisplayW.writeInt32Field(
|
|
1634
|
-
1,
|
|
1635
|
-
4
|
|
1636
|
-
) // displayMode
|
|
1637
|
-
dashDisplayW.writeInt32Field(
|
|
1638
|
-
2,
|
|
1639
|
-
3
|
|
1640
|
-
) // statusDisplayCount
|
|
1641
|
-
dashDisplayW.writeMessageField(
|
|
1642
|
-
3,
|
|
1643
|
-
byteArrayOf(1, 2, 3)
|
|
1644
|
-
) // statusDisplayOrder
|
|
1645
|
-
dashDisplayW.writeInt32Field(
|
|
1646
|
-
4,
|
|
1647
|
-
4
|
|
1648
|
-
) // widgetDisplayCount
|
|
1649
|
-
dashDisplayW.writeMessageField(
|
|
1650
|
-
5,
|
|
1651
|
-
byteArrayOf(1, 3, 2, 2)
|
|
1652
|
-
) // widgetDisplayOrder
|
|
1653
|
-
dashDisplayW.writeInt32Field(
|
|
1654
|
-
6,
|
|
1655
|
-
dashboardHalfDayFormat()
|
|
1656
|
-
) // halfDayFormat
|
|
1657
|
-
dashDisplayW.writeInt32Field(
|
|
1658
|
-
7,
|
|
1659
|
-
dashboardTemperatureUnit()
|
|
1660
|
-
) // temperatureUnit
|
|
1661
|
-
|
|
1662
|
-
val dashRecvW = ProtobufWriter()
|
|
1663
|
-
dashRecvW.writeMessageField(
|
|
1664
|
-
2,
|
|
1665
|
-
dashDisplayW.toByteArray()
|
|
1666
|
-
)
|
|
1667
|
-
|
|
1668
|
-
val dashPkgW = ProtobufWriter()
|
|
1669
|
-
dashPkgW.writeInt32Field(
|
|
1670
|
-
1,
|
|
1671
|
-
2
|
|
1672
|
-
) // Dashboard_Receive
|
|
1673
|
-
dashPkgW.writeInt32Field(
|
|
1674
|
-
2,
|
|
1675
|
-
sendManager.nextMagicRandom()
|
|
1676
|
-
)
|
|
1677
|
-
dashPkgW.writeMessageField(
|
|
1678
|
-
4,
|
|
1679
|
-
dashRecvW.toByteArray()
|
|
1680
|
-
)
|
|
1681
|
-
sendDashboardCommand(dashPkgW.toByteArray())
|
|
1682
|
-
|
|
1683
|
-
// 7. Dashboard REQUEST_NEWS_INFO (cmd=5,
|
|
1684
|
-
// field7={1:1})
|
|
1685
|
-
val dashNewsReqW = ProtobufWriter()
|
|
1686
|
-
dashNewsReqW.writeInt32Field(
|
|
1687
|
-
1,
|
|
1688
|
-
5
|
|
1689
|
-
) // REQUEST_NEWS_INFO
|
|
1690
|
-
dashNewsReqW.writeInt32Field(
|
|
1691
|
-
2,
|
|
1692
|
-
sendManager.nextMagicRandom()
|
|
1693
|
-
)
|
|
1694
|
-
dashNewsReqW.writeMessageField(
|
|
1695
|
-
7,
|
|
1696
|
-
byteArrayOf(0x08, 0x01)
|
|
1697
|
-
) // {1:1}
|
|
1698
|
-
sendDashboardCommand(
|
|
1699
|
-
dashNewsReqW.toByteArray()
|
|
1700
|
-
)
|
|
1701
|
-
|
|
1702
|
-
// 8. Gesture control list via g2_setting
|
|
1703
|
-
val gestListW = ProtobufWriter()
|
|
1704
|
-
gestListW.writeInt32Field(
|
|
1705
|
-
1,
|
|
1706
|
-
1
|
|
1707
|
-
) // DeviceReceiveInfo
|
|
1708
|
-
gestListW.writeInt32Field(
|
|
1709
|
-
2,
|
|
1710
|
-
sendManager.nextMagicRandom()
|
|
1711
|
-
)
|
|
1712
|
-
// field 3 with field 10
|
|
1713
|
-
// (gestureControlList): 3 items, all
|
|
1714
|
-
// app_unable
|
|
1715
|
-
val gestureCtrlPayload =
|
|
1716
|
-
byteArrayOf(
|
|
1717
|
-
0x52,
|
|
1718
|
-
0x18, // field 10, length 24
|
|
1719
|
-
0x0A,
|
|
1720
|
-
0x06,
|
|
1721
|
-
0x08,
|
|
1722
|
-
0x00,
|
|
1723
|
-
0x10,
|
|
1724
|
-
0x00,
|
|
1725
|
-
0x18,
|
|
1726
|
-
0x00, // item 1
|
|
1727
|
-
0x0A,
|
|
1728
|
-
0x06,
|
|
1729
|
-
0x08,
|
|
1730
|
-
0x00,
|
|
1731
|
-
0x10,
|
|
1732
|
-
0x01,
|
|
1733
|
-
0x18,
|
|
1734
|
-
0x00, // item 2
|
|
1735
|
-
0x0A,
|
|
1736
|
-
0x06,
|
|
1737
|
-
0x08,
|
|
1738
|
-
0x00,
|
|
1739
|
-
0x10,
|
|
1740
|
-
0x02,
|
|
1741
|
-
0x18,
|
|
1742
|
-
0x00 // item 3
|
|
1743
|
-
)
|
|
1744
|
-
gestListW.writeMessageField(
|
|
1745
|
-
3,
|
|
1746
|
-
gestureCtrlPayload
|
|
1747
|
-
)
|
|
1748
|
-
sendG2SettingCommand(
|
|
1749
|
-
gestListW.toByteArray()
|
|
1750
|
-
)
|
|
1751
|
-
|
|
1752
|
-
// 9. Dashboard APP_REQUEST_NEWS_INFO
|
|
1753
|
-
// (cmd=7, field9={1:1})
|
|
1754
|
-
val dashAppNewsW = ProtobufWriter()
|
|
1755
|
-
dashAppNewsW.writeInt32Field(
|
|
1756
|
-
1,
|
|
1757
|
-
7
|
|
1758
|
-
) // APP_REQUEST_NEWS_INFO
|
|
1759
|
-
dashAppNewsW.writeInt32Field(
|
|
1760
|
-
2,
|
|
1761
|
-
sendManager.nextMagicRandom()
|
|
1762
|
-
)
|
|
1763
|
-
dashAppNewsW.writeMessageField(
|
|
1764
|
-
9,
|
|
1765
|
-
byteArrayOf(0x08, 0x01)
|
|
1766
|
-
) // {1:1}
|
|
1767
|
-
sendDashboardCommand(
|
|
1768
|
-
dashAppNewsW.toByteArray()
|
|
1769
|
-
)
|
|
1770
|
-
|
|
1771
|
-
Bridge.log(
|
|
1772
|
-
"G2: Sent full Even-compatible init sequence"
|
|
1773
|
-
)
|
|
1774
|
-
},
|
|
1775
|
-
200
|
|
1776
|
-
)
|
|
1777
|
-
|
|
1778
|
-
// Start heartbeats after auth
|
|
1779
|
-
startHeartbeats()
|
|
1780
|
-
|
|
1781
|
-
// Mark as ready and request device info
|
|
1782
|
-
mainHandler.postDelayed(
|
|
1783
|
-
{
|
|
1784
|
-
reconnectionManager.stop()
|
|
1785
|
-
Bridge.log(
|
|
1786
|
-
"G2: Auth sequence complete, glasses ready"
|
|
1787
|
-
)
|
|
1788
|
-
|
|
1789
|
-
// Set device_name so DeviceManager can save
|
|
1790
|
-
// it for reconnection
|
|
1791
|
-
val peripheralName =
|
|
1792
|
-
rightGatt?.device?.name
|
|
1793
|
-
?: leftGatt?.device?.name
|
|
1794
|
-
val serialNumber =
|
|
1795
|
-
peripheralName?.let {
|
|
1796
|
-
deviceNameToSerialNumber[it]
|
|
1797
|
-
}
|
|
1798
|
-
if (serialNumber != null) {
|
|
1799
|
-
DeviceStore.apply(
|
|
1800
|
-
"bluetooth",
|
|
1801
|
-
"device_name",
|
|
1802
|
-
serialNumber
|
|
1803
|
-
)
|
|
1804
|
-
Bridge.log(
|
|
1805
|
-
"G2: Set device_name to $serialNumber"
|
|
1806
|
-
)
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
// Set bluetooth name and device model for
|
|
1810
|
-
// Device Info page
|
|
1811
|
-
val btName =
|
|
1812
|
-
rightGatt?.device?.name
|
|
1813
|
-
?: leftGatt?.device?.name
|
|
1814
|
-
?: ""
|
|
1815
|
-
DeviceStore.apply(
|
|
1816
|
-
"glasses",
|
|
1817
|
-
"bluetoothName",
|
|
1818
|
-
btName
|
|
1819
|
-
)
|
|
1820
|
-
DeviceStore.apply(
|
|
1821
|
-
"glasses",
|
|
1822
|
-
"deviceModel",
|
|
1823
|
-
DeviceTypes.G2
|
|
1824
|
-
)
|
|
1825
|
-
|
|
1826
|
-
setFullyConnected()
|
|
1827
|
-
|
|
1828
|
-
// Connect a controller if we have one
|
|
1829
|
-
connectController()
|
|
1830
|
-
|
|
1831
|
-
// Query version + battery info from glasses
|
|
1832
|
-
requestDeviceInfo()
|
|
1833
|
-
|
|
1834
|
-
sendMenuApps()
|
|
1835
|
-
sendStoredCalendarEvents()
|
|
1836
|
-
},
|
|
1837
|
-
500
|
|
1838
|
-
)
|
|
1839
|
-
},
|
|
1840
|
-
200
|
|
1841
|
-
)
|
|
1842
|
-
},
|
|
1843
|
-
200
|
|
1844
|
-
)
|
|
1845
|
-
},
|
|
1846
|
-
200
|
|
1604
|
+
delay(200)
|
|
1605
|
+
val authR = DevSettingsProto.authCmd(sendManager.nextMagicRandom())
|
|
1606
|
+
sendDevSettingsCommand(authR, left = false, right = true)
|
|
1607
|
+
|
|
1608
|
+
delay(200)
|
|
1609
|
+
val roleChange = DevSettingsProto.pipeRoleChange(sendManager.nextMagicRandom())
|
|
1610
|
+
sendDevSettingsCommand(roleChange, left = false, right = true)
|
|
1611
|
+
|
|
1612
|
+
delay(200)
|
|
1613
|
+
val timeSync = DevSettingsProto.timeSync(sendManager.nextMagicRandom())
|
|
1614
|
+
sendDevSettingsCommand(timeSync)
|
|
1615
|
+
|
|
1616
|
+
// Skip onboarding on connect
|
|
1617
|
+
delay(200)
|
|
1618
|
+
val onboarding = OnboardingProto.skipOnboarding(sendManager.nextMagicRandom())
|
|
1619
|
+
sendOnboardingCommand(onboarding)
|
|
1620
|
+
Bridge.log("G2: Sent onboarding skip (FINISH)")
|
|
1621
|
+
|
|
1622
|
+
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
1623
|
+
val gestureInitW = ProtobufWriter()
|
|
1624
|
+
gestureInitW.writeInt32Field(1, 0)
|
|
1625
|
+
gestureInitW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1626
|
+
sendGestureCtrlCommand(gestureInitW.toByteArray())
|
|
1627
|
+
|
|
1628
|
+
// 2. ui_setting_app (0x0C) — query (cmd=2, field4={settingInfoType=1, autoBrightnessLevel=0})
|
|
1629
|
+
val uiSettW = ProtobufWriter()
|
|
1630
|
+
uiSettW.writeInt32Field(1, 2) // cmd = DeviceReceiveRequest
|
|
1631
|
+
uiSettW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1632
|
+
uiSettW.writeMessageField(4, byteArrayOf(0x08, 0x01, 0x10, 0x00)) // {1:1, 2:0}
|
|
1633
|
+
sendToGlasses(
|
|
1634
|
+
sendManager.buildPackets(
|
|
1635
|
+
serviceId = 0x0C,
|
|
1636
|
+
payload = uiSettW.toByteArray(),
|
|
1637
|
+
reserveFlag = true
|
|
1638
|
+
)
|
|
1847
1639
|
)
|
|
1848
|
-
}
|
|
1849
1640
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1641
|
+
// 6. Dashboard init (0x01) — display settings
|
|
1642
|
+
// halfDayFormat: 1 = 12h, 0 = 24h
|
|
1643
|
+
// temperatureUnit: 1 = Celsius (metric), 2 = Fahrenheit (imperial)
|
|
1644
|
+
val dashDisplayW = ProtobufWriter()
|
|
1645
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1646
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1647
|
+
dashDisplayW.writeMessageField(3, byteArrayOf(1, 2, 3)) // statusDisplayOrder
|
|
1648
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1649
|
+
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
1650
|
+
dashDisplayW.writeMessageField(
|
|
1651
|
+
5,
|
|
1652
|
+
byteArrayOf(3, 1, 2, 4, 5)
|
|
1653
|
+
) // widgetDisplayOrder: Schedule, News, Stock, Quicklist
|
|
1654
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
1655
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
1852
1656
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1657
|
+
val dashRecvW = ProtobufWriter()
|
|
1658
|
+
dashRecvW.writeMessageField(2, dashDisplayW.toByteArray())
|
|
1659
|
+
|
|
1660
|
+
val dashPkgW = ProtobufWriter()
|
|
1661
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1662
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1663
|
+
dashPkgW.writeMessageField(4, dashRecvW.toByteArray())
|
|
1664
|
+
sendDashboardCommand(dashPkgW.toByteArray())
|
|
1665
|
+
|
|
1666
|
+
// Disable "Hey Even" wakeword on connect
|
|
1667
|
+
val heyEvenOff = EvenAIProto.setHeyEven(sendManager.nextMagicRandom(), false)
|
|
1668
|
+
sendEvenAICommand(heyEvenOff)
|
|
1669
|
+
Bridge.log("G2: Disabled Hey Even wakeword")
|
|
1670
|
+
|
|
1671
|
+
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
1672
|
+
|
|
1673
|
+
// Start heartbeats after auth
|
|
1674
|
+
startHeartbeats()
|
|
1675
|
+
|
|
1676
|
+
reconnectionManager.stop()
|
|
1677
|
+
Bridge.log("G2: Auth sequence complete, glasses ready")
|
|
1678
|
+
|
|
1679
|
+
// Set device_name so DeviceManager can save it for reconnection
|
|
1680
|
+
val peripheralName = rightGatt?.device?.name ?: leftGatt?.device?.name
|
|
1681
|
+
val serialNumber = peripheralName?.let { deviceNameToSerialNumber[it] }
|
|
1682
|
+
if (serialNumber != null) {
|
|
1683
|
+
DeviceStore.apply("bluetooth", "device_name", serialNumber)
|
|
1684
|
+
Bridge.log("G2: Set device_name to $serialNumber")
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Set bluetooth name and device model for Device Info page
|
|
1688
|
+
val btName = rightGatt?.device?.name ?: leftGatt?.device?.name ?: ""
|
|
1689
|
+
DeviceStore.apply("glasses", "bluetoothName", btName)
|
|
1690
|
+
DeviceStore.apply("glasses", "deviceModel", DeviceTypes.G2)
|
|
1691
|
+
|
|
1692
|
+
setFullyConnected()
|
|
1693
|
+
|
|
1694
|
+
// Connect a controller if we have one
|
|
1695
|
+
connectController()
|
|
1696
|
+
|
|
1697
|
+
// Query version + battery info from glasses
|
|
1698
|
+
requestDeviceInfo()
|
|
1699
|
+
|
|
1700
|
+
sendMenuApps()
|
|
1701
|
+
sendStoredCalendarEvents()
|
|
1702
|
+
|
|
1703
|
+
// Re-apply the IMU preference: the store only pushes imu_enabled to the glasses when
|
|
1704
|
+
// the value changes, so after a reconnect an already-on IMU would otherwise stay off
|
|
1705
|
+
// (accel_event stops) until the user toggles the setting again.
|
|
1706
|
+
val imuEnabled = DeviceStore.get("bluetooth", "imu_enabled") as? Boolean ?: false
|
|
1707
|
+
if (imuEnabled) {
|
|
1708
|
+
Bridge.log("G2: re-applying imu_enabled=true after connect")
|
|
1709
|
+
setImuEnabled(true)
|
|
1710
|
+
}
|
|
1890
1711
|
}
|
|
1891
1712
|
|
|
1892
1713
|
private fun dashboardHalfDayFormat(): Int {
|
|
@@ -1922,18 +1743,18 @@ class G2 : SGCManager() {
|
|
|
1922
1743
|
// ---------- Heartbeats ----------
|
|
1923
1744
|
|
|
1924
1745
|
private fun startHeartbeats() {
|
|
1925
|
-
// EvenHub heartbeat every
|
|
1746
|
+
// EvenHub heartbeat every 10 seconds
|
|
1926
1747
|
stopHeartbeats()
|
|
1927
1748
|
|
|
1928
1749
|
val hbRunnable =
|
|
1929
1750
|
object : Runnable {
|
|
1930
1751
|
override fun run() {
|
|
1931
1752
|
sendEvenHubHeartbeat()
|
|
1932
|
-
mainHandler.postDelayed(this,
|
|
1753
|
+
mainHandler.postDelayed(this, 10000)
|
|
1933
1754
|
}
|
|
1934
1755
|
}
|
|
1935
1756
|
heartbeatRunnable = hbRunnable
|
|
1936
|
-
mainHandler.postDelayed(hbRunnable,
|
|
1757
|
+
mainHandler.postDelayed(hbRunnable, 10000)
|
|
1937
1758
|
|
|
1938
1759
|
// DevSettings heartbeat every 5 seconds
|
|
1939
1760
|
val dsRunnable =
|
|
@@ -2018,35 +1839,130 @@ class G2 : SGCManager() {
|
|
|
2018
1839
|
// ---------- SGCManager: Display Control ----------
|
|
2019
1840
|
|
|
2020
1841
|
override fun sendTextWall(text: String) {
|
|
2021
|
-
|
|
1842
|
+
displayScope.launch {
|
|
1843
|
+
displayMutex.withLock {
|
|
1844
|
+
sendText(
|
|
1845
|
+
text,
|
|
1846
|
+
x = defaultTextX,
|
|
1847
|
+
y = defaultTextY,
|
|
1848
|
+
width = defaultTextWidth,
|
|
1849
|
+
height = defaultTextHeight,
|
|
1850
|
+
borderWidth = defaultTextBorderWidth,
|
|
1851
|
+
borderColor = defaultTextBorderColor,
|
|
1852
|
+
borderRadius = defaultTextBorderRadius,
|
|
1853
|
+
paddingLength = defaultTextPaddingLength
|
|
1854
|
+
)
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
private suspend fun sendText(
|
|
1860
|
+
text: String,
|
|
1861
|
+
x: Int? = null,
|
|
1862
|
+
y: Int? = null,
|
|
1863
|
+
width: Int? = null,
|
|
1864
|
+
height: Int? = null,
|
|
1865
|
+
borderWidth: Int? = null,
|
|
1866
|
+
borderColor: Int? = null,
|
|
1867
|
+
borderRadius: Int? = null,
|
|
1868
|
+
paddingLength: Int? = null
|
|
1869
|
+
) {
|
|
1870
|
+
// Bridge.log("G2: sendTextWall(${text.take(50)}...)")
|
|
2022
1871
|
|
|
2023
1872
|
// ignore events while the ER dashboard is open:
|
|
2024
|
-
val useNativeDashboard =
|
|
1873
|
+
val useNativeDashboard =
|
|
1874
|
+
DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
2025
1875
|
if (useNativeDashboard && dashboardShowing > 0) {
|
|
2026
1876
|
return
|
|
2027
1877
|
}
|
|
2028
1878
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
1879
|
+
val rx = x ?: defaultTextX
|
|
1880
|
+
val ry = y ?: defaultTextY
|
|
1881
|
+
val rw = width ?: defaultTextWidth
|
|
1882
|
+
val rh = height ?: defaultTextHeight
|
|
1883
|
+
val rBorderWidth = defaultTextBorderWidth
|
|
1884
|
+
val rBorderColor = defaultTextBorderColor
|
|
1885
|
+
val rBorderRadius = defaultTextBorderRadius
|
|
1886
|
+
val rPaddingLength = defaultTextPaddingLength
|
|
1887
|
+
val content = if (text.isEmpty()) " " else text
|
|
2033
1888
|
|
|
2034
|
-
if
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
1889
|
+
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
1890
|
+
val container: TextContainer
|
|
1891
|
+
val existingIndex =
|
|
1892
|
+
textContainers.indexOfFirst {
|
|
1893
|
+
it.matches(
|
|
1894
|
+
rx,
|
|
1895
|
+
ry,
|
|
1896
|
+
rw,
|
|
1897
|
+
rh,
|
|
1898
|
+
rBorderWidth,
|
|
1899
|
+
rBorderColor,
|
|
1900
|
+
rBorderRadius,
|
|
1901
|
+
rPaddingLength
|
|
1902
|
+
)
|
|
1903
|
+
}
|
|
1904
|
+
if (existingIndex >= 0) {
|
|
1905
|
+
textContainers[existingIndex].content = content
|
|
1906
|
+
container = textContainers[existingIndex]
|
|
1907
|
+
Bridge.log("G2: sendText() - reusing container ${container.id} for rect $rx,$ry ${rw}x$rh")
|
|
1908
|
+
if (!pageCreated) {
|
|
1909
|
+
rebuildPage()
|
|
1910
|
+
return
|
|
1911
|
+
}
|
|
1912
|
+
// update the text container:
|
|
1913
|
+
val msg =
|
|
1914
|
+
EvenHubProto.updateTextMessage(
|
|
1915
|
+
containerID = container.id,
|
|
1916
|
+
contentOffset = 0,
|
|
1917
|
+
contentLength = container.content.toByteArray(Charsets.UTF_8).size,
|
|
1918
|
+
content = container.content
|
|
1919
|
+
)
|
|
1920
|
+
sendEvenHubCommand(msg)
|
|
1921
|
+
return
|
|
2038
1922
|
}
|
|
1923
|
+
container =
|
|
1924
|
+
addTextContainer(
|
|
1925
|
+
rx,
|
|
1926
|
+
ry,
|
|
1927
|
+
rw,
|
|
1928
|
+
rh,
|
|
1929
|
+
content,
|
|
1930
|
+
rBorderWidth,
|
|
1931
|
+
rBorderColor,
|
|
1932
|
+
rBorderRadius,
|
|
1933
|
+
rPaddingLength
|
|
1934
|
+
)
|
|
1935
|
+
Bridge.log("G2: sendText() - added text container ${container.id} for rect $rx,$ry ${rw}x$rh, rebuilding page")
|
|
1936
|
+
rebuildPage()
|
|
2039
1937
|
}
|
|
2040
1938
|
|
|
2041
1939
|
override fun sendDoubleTextWall(top: String, bottom: String) {
|
|
2042
|
-
|
|
1940
|
+
Bridge.log("G2: sendDoubleTextWall() - top: $top, bottom: $bottom")
|
|
1941
|
+
// G2 doesn't have native double text wall, combine them
|
|
1942
|
+
val combined = "$top\n\n$bottom"
|
|
2043
1943
|
sendTextWall(combined)
|
|
2044
1944
|
}
|
|
2045
1945
|
|
|
2046
1946
|
override fun clearDisplay() {
|
|
2047
1947
|
Bridge.log("G2: clearDisplay()")
|
|
2048
|
-
|
|
2049
|
-
|
|
1948
|
+
displayScope.launch {
|
|
1949
|
+
displayMutex.withLock {
|
|
1950
|
+
// reset the content of all text containers to empty:
|
|
1951
|
+
for (i in textContainers.indices) {
|
|
1952
|
+
textContainers[i].content = " "
|
|
1953
|
+
}
|
|
1954
|
+
for (i in imageContainers.indices) {
|
|
1955
|
+
imageContainers[i].bmpData = ByteArray(0)
|
|
1956
|
+
}
|
|
1957
|
+
// shutdown the page and then recreate the containers without the content.
|
|
1958
|
+
// Reset pageCreated so the recreate below issues a fresh createPageMessage rather
|
|
1959
|
+
// than a rebuildPageMessage that no longer matches the torn-down page.
|
|
1960
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
1961
|
+
sendEvenHubCommand(msg)
|
|
1962
|
+
pageCreated = false
|
|
1963
|
+
createPageWithContainers()
|
|
1964
|
+
restartMicIfAlreadyEnabled()
|
|
1965
|
+
}
|
|
2050
1966
|
}
|
|
2051
1967
|
}
|
|
2052
1968
|
|
|
@@ -2068,13 +1984,16 @@ class G2 : SGCManager() {
|
|
|
2068
1984
|
width: Int?,
|
|
2069
1985
|
height: Int?
|
|
2070
1986
|
): Boolean {
|
|
2071
|
-
currentBitmapBase64 = base64ImageData
|
|
2072
|
-
currentTextContent = ""
|
|
2073
|
-
|
|
2074
1987
|
val rx = x ?: defaultImgX
|
|
2075
1988
|
val ry = y ?: defaultImgY
|
|
2076
1989
|
val rw = width ?: defaultImgWidth
|
|
2077
1990
|
val rh = height ?: defaultImgHeight
|
|
1991
|
+
|
|
1992
|
+
// ignore events while the ER dashboard is open:
|
|
1993
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
1994
|
+
if (useNativeDashboard && dashboardShowing > 0) {
|
|
1995
|
+
return false
|
|
1996
|
+
}
|
|
2078
1997
|
|
|
2079
1998
|
val rawData =
|
|
2080
1999
|
Base64.decode(base64ImageData, Base64.DEFAULT)
|
|
@@ -2083,54 +2002,38 @@ class G2 : SGCManager() {
|
|
|
2083
2002
|
return false
|
|
2084
2003
|
}
|
|
2085
2004
|
|
|
2086
|
-
// Create the startup page lazily, seeded with the default top-left container.
|
|
2087
|
-
val freshPage = !startupPageCreated
|
|
2088
|
-
if (freshPage) {
|
|
2089
|
-
imageContainers.clear()
|
|
2090
|
-
imageContainers.add(
|
|
2091
|
-
ImgContainer(
|
|
2092
|
-
id = imageContainerIDPool[0],
|
|
2093
|
-
x = defaultImgX,
|
|
2094
|
-
y = defaultImgY,
|
|
2095
|
-
width = defaultImgWidth,
|
|
2096
|
-
height = defaultImgHeight
|
|
2097
|
-
)
|
|
2098
|
-
)
|
|
2099
|
-
createPageWithText("")
|
|
2100
|
-
Bridge.log("G2: displayBitmap() - startup page created")
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
2104
|
-
val existing = imageContainers.firstOrNull { it.matches(rx, ry, rw, rh) }
|
|
2105
|
-
val container: ImgContainer
|
|
2106
|
-
val needsRebuild: Boolean
|
|
2107
|
-
if (existing != null) {
|
|
2108
|
-
container = existing
|
|
2109
|
-
needsRebuild = false
|
|
2110
|
-
Bridge.log("G2: displayBitmap() - reusing container ${existing.id} for rect $rx,$ry ${rw}x$rh")
|
|
2111
|
-
} else {
|
|
2112
|
-
container = addImageContainer(rx, ry, rw, rh)
|
|
2113
|
-
needsRebuild = true
|
|
2114
|
-
Bridge.log("G2: displayBitmap() - added container ${container.id} for rect $rx,$ry ${rw}x$rh, rebuilding page")
|
|
2115
|
-
rebuildPage()
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
2005
|
val bmpData =
|
|
2119
|
-
convertToG2Bmp(rawData, containerWidth =
|
|
2006
|
+
convertToG2Bmp(rawData, containerWidth = rw, containerHeight = rh)
|
|
2120
2007
|
?: run {
|
|
2121
2008
|
Bridge.log("G2: displayBitmap() - failed to convert image to BMP")
|
|
2122
2009
|
return false
|
|
2123
2010
|
}
|
|
2124
2011
|
|
|
2125
|
-
//
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
)
|
|
2012
|
+
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
2013
|
+
val container: ImgContainer
|
|
2014
|
+
val existingIndex = imageContainers.indexOfFirst { it.matches(rx, ry, rw, rh) }
|
|
2015
|
+
if (existingIndex >= 0) {
|
|
2016
|
+
imageContainers[existingIndex].bmpData = bmpData
|
|
2017
|
+
container = imageContainers[existingIndex]
|
|
2018
|
+
Bridge.log("G2: displayBitmap() - reusing container ${container.id} for rect $rx,$ry ${rw}x$rh")
|
|
2019
|
+
displayScope.launch {
|
|
2020
|
+
displayMutex.withLock {
|
|
2021
|
+
if (!pageCreated) {
|
|
2022
|
+
rebuildPage()
|
|
2023
|
+
} else {
|
|
2024
|
+
sendImageData(container.id, container.name, container.bmpData)
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return true
|
|
2132
2029
|
} else {
|
|
2133
|
-
|
|
2030
|
+
container = addImageContainer(rx, ry, rw, rh, bmpData)
|
|
2031
|
+
Bridge.log("G2: displayBitmap() - added container ${container.id} for rect $rx,$ry ${rw}x$rh, rebuilding page")
|
|
2032
|
+
displayScope.launch {
|
|
2033
|
+
displayMutex.withLock {
|
|
2034
|
+
rebuildPage()
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2134
2037
|
}
|
|
2135
2038
|
|
|
2136
2039
|
return true
|
|
@@ -2140,7 +2043,13 @@ class G2 : SGCManager() {
|
|
|
2140
2043
|
* Add a new image container for the rect, evicting the oldest when the list is full (max 4).
|
|
2141
2044
|
* Returns the newly tracked container (with an assigned ID from the pool).
|
|
2142
2045
|
*/
|
|
2143
|
-
private fun addImageContainer(
|
|
2046
|
+
private fun addImageContainer(
|
|
2047
|
+
x: Int,
|
|
2048
|
+
y: Int,
|
|
2049
|
+
width: Int,
|
|
2050
|
+
height: Int,
|
|
2051
|
+
bmpData: ByteArray
|
|
2052
|
+
): ImgContainer {
|
|
2144
2053
|
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2145
2054
|
if (imageContainers.size >= imageContainerIDPool.size) {
|
|
2146
2055
|
val evicted = imageContainers.removeAt(0)
|
|
@@ -2149,21 +2058,106 @@ class G2 : SGCManager() {
|
|
|
2149
2058
|
// Pick the lowest free ID from the pool.
|
|
2150
2059
|
val usedIDs = imageContainers.map { it.id }.toSet()
|
|
2151
2060
|
val id = imageContainerIDPool.firstOrNull { it !in usedIDs } ?: imageContainerIDPool[0]
|
|
2152
|
-
val container =
|
|
2061
|
+
val container =
|
|
2062
|
+
ImgContainer(id = id, x = x, y = y, width = width, height = height, bmpData = bmpData)
|
|
2153
2063
|
imageContainers.add(container)
|
|
2154
2064
|
return container
|
|
2155
2065
|
}
|
|
2156
2066
|
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2067
|
+
private fun addTextContainer(
|
|
2068
|
+
x: Int,
|
|
2069
|
+
y: Int,
|
|
2070
|
+
width: Int,
|
|
2071
|
+
height: Int,
|
|
2072
|
+
content: String,
|
|
2073
|
+
borderWidth: Int,
|
|
2074
|
+
borderColor: Int,
|
|
2075
|
+
borderRadius: Int,
|
|
2076
|
+
paddingLength: Int
|
|
2077
|
+
): TextContainer {
|
|
2078
|
+
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2079
|
+
if (textContainers.size >= textContainerIDPool.size) {
|
|
2080
|
+
val evicted = textContainers.removeAt(0)
|
|
2081
|
+
Bridge.log("G2: evicting oldest text container ${evicted.id}")
|
|
2082
|
+
}
|
|
2083
|
+
// Pick the lowest free ID from the pool.
|
|
2084
|
+
val usedIDs = textContainers.map { it.id }.toSet()
|
|
2085
|
+
val id = textContainerIDPool.firstOrNull { it !in usedIDs } ?: textContainerIDPool[0]
|
|
2086
|
+
val container =
|
|
2087
|
+
TextContainer(
|
|
2088
|
+
id = id,
|
|
2089
|
+
x = x,
|
|
2090
|
+
y = y,
|
|
2091
|
+
width = width,
|
|
2092
|
+
height = height,
|
|
2093
|
+
content = content,
|
|
2094
|
+
borderWidth = borderWidth,
|
|
2095
|
+
borderColor = borderColor,
|
|
2096
|
+
borderRadius = borderRadius,
|
|
2097
|
+
paddingLength = paddingLength
|
|
2098
|
+
)
|
|
2099
|
+
textContainers.add(container)
|
|
2100
|
+
return container
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
/**
|
|
2104
|
+
* Shutdown and rebuild everything, re-sending all data to the glasses.
|
|
2105
|
+
*
|
|
2106
|
+
* Suspends until the rebuild (shutdown → create → image/text re-send) has been issued, so
|
|
2107
|
+
* callers no longer race a detached coroutine. Always invoke from within [displayMutex].
|
|
2108
|
+
*/
|
|
2109
|
+
private suspend fun rebuildPage() {
|
|
2110
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
2111
|
+
sendEvenHubCommand(msg)
|
|
2112
|
+
pageCreated = false
|
|
2113
|
+
rebuildState()
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* Re-creates the containers and sends all images and text again to the glasses.
|
|
2118
|
+
*
|
|
2119
|
+
* Runs inline (suspending) rather than launching a detached coroutine, so the page is fully
|
|
2120
|
+
* rebuilt before the caller continues. Always invoke from within [displayMutex].
|
|
2121
|
+
*/
|
|
2122
|
+
private suspend fun rebuildState() {
|
|
2123
|
+
Bridge.log("G2: rebuildState()")
|
|
2124
|
+
// recreate the containers:
|
|
2125
|
+
createPageWithContainers()
|
|
2126
|
+
|
|
2127
|
+
delay(300) // 300ms to settle
|
|
2128
|
+
|
|
2129
|
+
// go through each image container and send the data:
|
|
2130
|
+
for (container in imageContainers) {
|
|
2131
|
+
sendImageData(container.id, container.name, container.bmpData)
|
|
2132
|
+
delay(300) // 300ms between containers
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// go through each text container and re-send its content, otherwise text stays blank after a
|
|
2136
|
+
// disabled because text containers are initialized with their content:
|
|
2137
|
+
// for (container in textContainers) {
|
|
2138
|
+
// val textMsg =
|
|
2139
|
+
// EvenHubProto.updateTextMessage(
|
|
2140
|
+
// containerID = container.id,
|
|
2141
|
+
// contentOffset = 0,
|
|
2142
|
+
// contentLength = container.content.toByteArray(Charsets.UTF_8).size,
|
|
2143
|
+
// content = container.content
|
|
2144
|
+
// )
|
|
2145
|
+
// sendEvenHubCommand(textMsg)
|
|
2146
|
+
// delay(100)
|
|
2147
|
+
// }
|
|
2160
2148
|
}
|
|
2161
2149
|
|
|
2162
|
-
|
|
2150
|
+
/**
|
|
2151
|
+
* Send a bitmap to an image container as fragmented updateImageRawData packets.
|
|
2152
|
+
*
|
|
2153
|
+
* Suspends until every fragment has been sent, mirroring iOS `sendImageData` (which awaits
|
|
2154
|
+
* 200ms after every fragment, including the last). Callers therefore get the same serialized
|
|
2155
|
+
* timing window: the 300ms settle in [rebuildState] only runs once all fragments are out.
|
|
2156
|
+
*/
|
|
2157
|
+
private suspend fun sendImageData(
|
|
2163
2158
|
containerID: Int,
|
|
2164
2159
|
containerName: String,
|
|
2165
|
-
bmpData: ByteArray
|
|
2166
|
-
onComplete: (() -> Unit)? = null
|
|
2160
|
+
bmpData: ByteArray
|
|
2167
2161
|
) {
|
|
2168
2162
|
val fragmentSize = 4096
|
|
2169
2163
|
imageSessionCounter++
|
|
@@ -2172,15 +2166,9 @@ class G2 : SGCManager() {
|
|
|
2172
2166
|
var fragmentIndex = 0
|
|
2173
2167
|
var offset = 0
|
|
2174
2168
|
|
|
2175
|
-
|
|
2176
|
-
if (offset >= bmpData.size) {
|
|
2177
|
-
Bridge.log(
|
|
2178
|
-
"G2: sendImageData($containerName) - $fragmentIndex fragments, ${bmpData.size} bytes"
|
|
2179
|
-
)
|
|
2180
|
-
onComplete?.invoke()
|
|
2181
|
-
return
|
|
2182
|
-
}
|
|
2169
|
+
Bridge.log("G2: sendImageData($containerName) - $fragmentIndex fragments, ${bmpData.size} bytes")
|
|
2183
2170
|
|
|
2171
|
+
while (offset < bmpData.size) {
|
|
2184
2172
|
val end = minOf(offset + fragmentSize, bmpData.size)
|
|
2185
2173
|
val fragment = bmpData.copyOfRange(offset, end)
|
|
2186
2174
|
|
|
@@ -2200,20 +2188,8 @@ class G2 : SGCManager() {
|
|
|
2200
2188
|
|
|
2201
2189
|
fragmentIndex++
|
|
2202
2190
|
offset = end
|
|
2203
|
-
|
|
2204
|
-
// 200ms between fragments — and also before onComplete (matches iOS, which awaits
|
|
2205
|
-
// 200ms after every fragment including the last).
|
|
2206
|
-
if (offset < bmpData.size) {
|
|
2207
|
-
mainHandler.postDelayed({ sendNextFragment() }, 200)
|
|
2208
|
-
} else {
|
|
2209
|
-
Bridge.log(
|
|
2210
|
-
"G2: sendImageData($containerName) - $fragmentIndex fragments, ${bmpData.size} bytes"
|
|
2211
|
-
)
|
|
2212
|
-
mainHandler.postDelayed({ onComplete?.invoke() }, 200)
|
|
2213
|
-
}
|
|
2191
|
+
delay(200) // 200ms between fragments (and after the last, matching iOS)
|
|
2214
2192
|
}
|
|
2215
|
-
|
|
2216
|
-
sendNextFragment()
|
|
2217
2193
|
}
|
|
2218
2194
|
|
|
2219
2195
|
/// Bring the Even Realities dashboard (the OS-level home/idle screen) to
|
|
@@ -2225,8 +2201,6 @@ class G2 : SGCManager() {
|
|
|
2225
2201
|
val msg = EvenHubProto.shutdownMessage()
|
|
2226
2202
|
sendEvenHubCommand(msg)
|
|
2227
2203
|
pageCreated = false
|
|
2228
|
-
pageHasTextContainer = false
|
|
2229
|
-
currentTextContent = ""
|
|
2230
2204
|
currentBitmapBase64 = ""
|
|
2231
2205
|
mainHandler.postDelayed({
|
|
2232
2206
|
// activate the dashboard by setting depth to the current setting:
|
|
@@ -2332,7 +2306,7 @@ class G2 : SGCManager() {
|
|
|
2332
2306
|
* The SKILL step alone is ignored by firmware; the preceding ENTER+ASK
|
|
2333
2307
|
* supply the session context that lets the glasses act on the SKILL.
|
|
2334
2308
|
*/
|
|
2335
|
-
override fun showNotificationsPanel() {
|
|
2309
|
+
override suspend fun showNotificationsPanel() {
|
|
2336
2310
|
Bridge.log("G2: showNotificationsPanel()")
|
|
2337
2311
|
val enterPayload = EvenAIProto.aiCtrl(
|
|
2338
2312
|
magicRandom = sendManager.nextMagicRandom(),
|
|
@@ -2340,26 +2314,24 @@ class G2 : SGCManager() {
|
|
|
2340
2314
|
)
|
|
2341
2315
|
sendEvenAICommand(enterPayload)
|
|
2342
2316
|
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2317
|
+
delay(400)
|
|
2318
|
+
val askPayload = EvenAIProto.aiAsk(
|
|
2319
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2320
|
+
text = " ",
|
|
2321
|
+
streamEnable = 0
|
|
2322
|
+
)
|
|
2323
|
+
sendEvenAICommand(askPayload)
|
|
2350
2324
|
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
}, 400)
|
|
2362
|
-
}, 400)
|
|
2325
|
+
delay(400)
|
|
2326
|
+
val skillPayload = EvenAIProto.triggerSkill(
|
|
2327
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2328
|
+
skillId = 3, // NOTIFICATION
|
|
2329
|
+
skillParam = 1, // show
|
|
2330
|
+
text = " ",
|
|
2331
|
+
streamEnable = 1,
|
|
2332
|
+
fTextEnd = 1
|
|
2333
|
+
)
|
|
2334
|
+
sendEvenAICommand(skillPayload)
|
|
2363
2335
|
}
|
|
2364
2336
|
|
|
2365
2337
|
override fun setBrightness(level: Int, autoMode: Boolean) {
|
|
@@ -2375,22 +2347,45 @@ class G2 : SGCManager() {
|
|
|
2375
2347
|
|
|
2376
2348
|
// ---------- Private Display Helpers ----------
|
|
2377
2349
|
|
|
2378
|
-
private fun
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2350
|
+
private fun createPageWithContainers() {
|
|
2351
|
+
// build the page's text containers from the live tracked list.
|
|
2352
|
+
val textContainerProps: List<ByteArray> = ArrayList<ByteArray>(textContainers.size).apply {
|
|
2353
|
+
for (i in textContainers.indices) {
|
|
2354
|
+
val c = textContainers[i]
|
|
2355
|
+
add(
|
|
2356
|
+
EvenHubProto.textContainerProperty(
|
|
2357
|
+
x = c.x,
|
|
2358
|
+
y = c.y,
|
|
2359
|
+
width = c.width,
|
|
2360
|
+
height = c.height,
|
|
2361
|
+
borderWidth = c.borderWidth,
|
|
2362
|
+
borderColor = c.borderColor,
|
|
2363
|
+
borderRadius = c.borderRadius,
|
|
2364
|
+
paddingLength = c.paddingLength,
|
|
2365
|
+
containerID = c.id,
|
|
2366
|
+
containerName = c.name,
|
|
2367
|
+
isEventCapture = i == 0,// the first container is the event capture container
|
|
2368
|
+
content = c.content
|
|
2369
|
+
)
|
|
2393
2370
|
)
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
|
|
2375
|
+
// iterate all image containers, remove any entries with duplicate ids or empty data,
|
|
2376
|
+
// and ensure the ids are still in the imageContainerIDPool:
|
|
2377
|
+
val seenIDs = mutableSetOf<Int>()
|
|
2378
|
+
imageContainers.retainAll { c ->
|
|
2379
|
+
if (c.bmpData.isEmpty()) {
|
|
2380
|
+
Bridge.log("G2: removing empty image container ${c.id}")
|
|
2381
|
+
return@retainAll false
|
|
2382
|
+
}
|
|
2383
|
+
if (!seenIDs.add(c.id)) {
|
|
2384
|
+
Bridge.log("G2: removing duplicate image container ${c.id}")
|
|
2385
|
+
return@retainAll false
|
|
2386
|
+
}
|
|
2387
|
+
imageContainerIDPool.contains(c.id)
|
|
2388
|
+
}
|
|
2394
2389
|
|
|
2395
2390
|
// Build the page's image containers from the live tracked list.
|
|
2396
2391
|
val imageContainerProps: List<ByteArray> =
|
|
@@ -2406,21 +2401,20 @@ class G2 : SGCManager() {
|
|
|
2406
2401
|
}
|
|
2407
2402
|
|
|
2408
2403
|
val msg: ByteArray
|
|
2409
|
-
if (!
|
|
2410
|
-
Bridge.log("G2:
|
|
2404
|
+
if (!pageCreated) {
|
|
2405
|
+
Bridge.log("G2: using createPageMessage (first time)")
|
|
2411
2406
|
msg =
|
|
2412
2407
|
EvenHubProto.createPageMessage(
|
|
2413
|
-
textContainers =
|
|
2408
|
+
textContainers = textContainerProps,
|
|
2414
2409
|
imageContainers = imageContainerProps,
|
|
2415
2410
|
magicRandom = sendManager.nextMagicRandom(),
|
|
2416
2411
|
appId = activeMenuAppId
|
|
2417
2412
|
)
|
|
2418
|
-
startupPageCreated = true
|
|
2419
2413
|
} else {
|
|
2420
|
-
Bridge.log("G2:
|
|
2414
|
+
Bridge.log("G2: using rebuildPageMessage")
|
|
2421
2415
|
msg =
|
|
2422
2416
|
EvenHubProto.rebuildPageMessage(
|
|
2423
|
-
textContainers =
|
|
2417
|
+
textContainers = textContainerProps,
|
|
2424
2418
|
imageContainers = imageContainerProps,
|
|
2425
2419
|
magicRandom = sendManager.nextMagicRandom(),
|
|
2426
2420
|
appId = activeMenuAppId
|
|
@@ -2428,22 +2422,6 @@ class G2 : SGCManager() {
|
|
|
2428
2422
|
}
|
|
2429
2423
|
sendEvenHubCommand(msg)
|
|
2430
2424
|
pageCreated = true
|
|
2431
|
-
pageHasTextContainer = true
|
|
2432
|
-
currentTextContent = text
|
|
2433
|
-
currentBitmapBase64 = ""
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
private fun updateText(text: String) {
|
|
2437
|
-
val msg =
|
|
2438
|
-
EvenHubProto.updateTextMessage(
|
|
2439
|
-
containerID = textContainerID,
|
|
2440
|
-
contentOffset = 0,
|
|
2441
|
-
contentLength = text.toByteArray(Charsets.UTF_8).size,
|
|
2442
|
-
content = text
|
|
2443
|
-
)
|
|
2444
|
-
queueEvenHubCommand(msg)
|
|
2445
|
-
currentTextContent = text
|
|
2446
|
-
currentBitmapBase64 = ""
|
|
2447
2425
|
}
|
|
2448
2426
|
|
|
2449
2427
|
@Synchronized
|
|
@@ -2593,9 +2571,9 @@ class G2 : SGCManager() {
|
|
|
2593
2571
|
bmp.write(rowBuf)
|
|
2594
2572
|
}
|
|
2595
2573
|
|
|
2596
|
-
Bridge.log(
|
|
2597
|
-
|
|
2598
|
-
)
|
|
2574
|
+
// Bridge.log(
|
|
2575
|
+
// "G2: build4BitBmp - ${bmp.size()} bytes (header=$headerSize, pixels=$pixelDataSize, rows=${paddedRowSize}x$height)"
|
|
2576
|
+
// )
|
|
2599
2577
|
return bmp.toByteArray()
|
|
2600
2578
|
}
|
|
2601
2579
|
|
|
@@ -2613,6 +2591,13 @@ class G2 : SGCManager() {
|
|
|
2613
2591
|
|
|
2614
2592
|
// ---------- SGCManager: Audio Control ----------
|
|
2615
2593
|
|
|
2594
|
+
private fun restartMicIfAlreadyEnabled() {
|
|
2595
|
+
val currentEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
2596
|
+
if (currentEnabled) {
|
|
2597
|
+
restartMic()
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2616
2601
|
fun restartMic() {
|
|
2617
2602
|
// if already enabled, set to disabled, then send enabled after 500ms:
|
|
2618
2603
|
DeviceStore.apply("glasses", "micEnabled", true)
|
|
@@ -2620,11 +2605,11 @@ class G2 : SGCManager() {
|
|
|
2620
2605
|
sendEvenHubCommand(msg)
|
|
2621
2606
|
mainHandler.postDelayed({
|
|
2622
2607
|
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
2623
|
-
Bridge.log("G2: setMicEnabled - useNativeDashboard=$useNativeDashboard, dashboardShowing=$dashboardShowing")
|
|
2608
|
+
// Bridge.log("G2: setMicEnabled - useNativeDashboard=$useNativeDashboard, dashboardShowing=$dashboardShowing")
|
|
2624
2609
|
if (useNativeDashboard && dashboardShowing > 0) {
|
|
2625
2610
|
return@postDelayed
|
|
2626
2611
|
}
|
|
2627
|
-
if (!pageCreated
|
|
2612
|
+
if (!pageCreated) {
|
|
2628
2613
|
DeviceManager.getInstance().sendCurrentState() // should re-create the page if needed
|
|
2629
2614
|
}
|
|
2630
2615
|
val msg = EvenHubProto.audioControlMessage(true)
|
|
@@ -2762,8 +2747,8 @@ class G2 : SGCManager() {
|
|
|
2762
2747
|
rightAuthenticated = false
|
|
2763
2748
|
startupPageCreated = false
|
|
2764
2749
|
pageCreated = false
|
|
2765
|
-
pageHasTextContainer = false
|
|
2766
2750
|
imageContainers.clear()
|
|
2751
|
+
textContainers.clear()
|
|
2767
2752
|
dashboardShowing = 0
|
|
2768
2753
|
heartbeatCounter = 0
|
|
2769
2754
|
currentBitmapBase64 = ""
|
|
@@ -2807,10 +2792,78 @@ class G2 : SGCManager() {
|
|
|
2807
2792
|
}
|
|
2808
2793
|
|
|
2809
2794
|
override fun dbg1() {
|
|
2810
|
-
showNotificationsPanel()
|
|
2795
|
+
// showNotificationsPanel()
|
|
2811
2796
|
}
|
|
2797
|
+
|
|
2798
|
+
private var compassRunning = false
|
|
2799
|
+
|
|
2812
2800
|
override fun dbg2() {
|
|
2813
|
-
|
|
2801
|
+
// compassRunning = !compassRunning
|
|
2802
|
+
// Bridge.log("G2: dbg2() — ${if (compassRunning) "start" else "stop"} compass")
|
|
2803
|
+
// if (compassRunning) {
|
|
2804
|
+
// startCompass()
|
|
2805
|
+
// } else {
|
|
2806
|
+
// stopCompass()
|
|
2807
|
+
// }
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
/**
|
|
2811
|
+
* Start a navigation session so the glasses stream compass heading via
|
|
2812
|
+
* OS_NOTIFY_COMPASS_CHANGED — surfaced as `CompassHeadingEvent { heading: 0…359 }` in
|
|
2813
|
+
* handleNavigationResponse.
|
|
2814
|
+
*
|
|
2815
|
+
* If the magnetometer needs calibration, the glasses emit OS_NOTIFY_COMPASS_CALIBRATE_STRAT
|
|
2816
|
+
* (→ `CompassCalibrationEvent {status:"start"}`); the wearer should look around until
|
|
2817
|
+
* `…{status:"complete"}`.
|
|
2818
|
+
*/
|
|
2819
|
+
fun startCompass() {
|
|
2820
|
+
val w = ProtobufWriter()
|
|
2821
|
+
w.writeInt32Field(1, NavigationCmd.APP_REQUEST_START_UP.value) // cmd
|
|
2822
|
+
w.writeInt32Field(2, sendManager.nextMagicRandom()) // magicRandom
|
|
2823
|
+
sendNavigationCommand(w.toByteArray())
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
/** Stop the navigation/compass session (ends heading streaming). */
|
|
2827
|
+
fun stopCompass() {
|
|
2828
|
+
val w = ProtobufWriter()
|
|
2829
|
+
w.writeInt32Field(1, NavigationCmd.APP_REQUEST_EXIT.value)
|
|
2830
|
+
w.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
2831
|
+
sendNavigationCommand(w.toByteArray())
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
override suspend fun setImuEnabled(enabled: Boolean) {
|
|
2835
|
+
setImuEnabled(enabled, reportFrq = EvenHubProto.IMU_PACE_P100)
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
/**
|
|
2839
|
+
* Enable or disable IMU motion reporting on the glasses.
|
|
2840
|
+
*
|
|
2841
|
+
* When enabled, the glasses continuously push `IMU_Report_Data { x, y, z }` (32-bit floats,
|
|
2842
|
+
* gravity-normalized) via the EvenHub notify path; these surface in `handleTouchEvent` as a
|
|
2843
|
+
* Sys_ItemEvent with `eventType == IMU_DATA_REPORT (8)` and are emitted through
|
|
2844
|
+
* `Bridge.sendAccelEvent` (a single accelerometer reading; a richer combined IMU event
|
|
2845
|
+
* covering gyro + magnetometer is future work).
|
|
2846
|
+
*/
|
|
2847
|
+
suspend fun setImuEnabled(enabled: Boolean, reportFrq: Int) {
|
|
2848
|
+
Bridge.log("G2: setImuEnabled($enabled, frq=$reportFrq)")
|
|
2849
|
+
|
|
2850
|
+
displayMutex.withLock {
|
|
2851
|
+
// IMU requires an active EvenHub page (same prerequisite as the mic). Await the
|
|
2852
|
+
// rebuild so the control packet is sent only after the page actually exists — page
|
|
2853
|
+
// creation is async with variable delays, so a fixed wait could send too early and
|
|
2854
|
+
// reporting would never start.
|
|
2855
|
+
if (enabled && !pageCreated) {
|
|
2856
|
+
rebuildState()
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
val msg =
|
|
2860
|
+
EvenHubProto.imuControlMessage(
|
|
2861
|
+
enable = enabled,
|
|
2862
|
+
reportFrq = reportFrq,
|
|
2863
|
+
magicRandom = sendManager.nextMagicRandom()
|
|
2864
|
+
)
|
|
2865
|
+
sendEvenHubCommand(msg)
|
|
2866
|
+
}
|
|
2814
2867
|
}
|
|
2815
2868
|
|
|
2816
2869
|
fun reconnectController() {
|
|
@@ -2899,7 +2952,12 @@ class G2 : SGCManager() {
|
|
|
2899
2952
|
}
|
|
2900
2953
|
|
|
2901
2954
|
override fun sendShutdown() {
|
|
2902
|
-
clearDisplay()
|
|
2955
|
+
// Send the EvenHub shutdown synchronously before tearing down BLE. clearDisplay() is now
|
|
2956
|
+
// fire-and-forget (it serializes on displayScope), so calling it here would let disconnect()
|
|
2957
|
+
// close the GATT before the deferred shutdown packet was ever written.
|
|
2958
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
2959
|
+
sendEvenHubCommand(msg)
|
|
2960
|
+
pageCreated = false
|
|
2903
2961
|
disconnect()
|
|
2904
2962
|
}
|
|
2905
2963
|
|
|
@@ -3248,7 +3306,6 @@ class G2 : SGCManager() {
|
|
|
3248
3306
|
|
|
3249
3307
|
startupPageCreated = false
|
|
3250
3308
|
pageCreated = false
|
|
3251
|
-
pageHasTextContainer = false
|
|
3252
3309
|
dashboardShowing = 0
|
|
3253
3310
|
DeviceStore.apply("glasses", "connected", false)
|
|
3254
3311
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
@@ -3335,7 +3392,7 @@ class G2 : SGCManager() {
|
|
|
3335
3392
|
stopScan()
|
|
3336
3393
|
authStarted = true
|
|
3337
3394
|
Bridge.log("G2: Both sides initialized, starting auth sequence")
|
|
3338
|
-
runAuthSequence()
|
|
3395
|
+
displayScope.launch { runAuthSequence() }
|
|
3339
3396
|
}
|
|
3340
3397
|
}
|
|
3341
3398
|
}
|
|
@@ -3415,6 +3472,8 @@ class G2 : SGCManager() {
|
|
|
3415
3472
|
ServiceID.MENU.value -> handleMenuResponse(payload)
|
|
3416
3473
|
ServiceID.DASHBOARD.value -> handleDashboardResponse(payload)
|
|
3417
3474
|
ServiceID.GESTURE_CTRL.value -> handleGestureCtrl(payload)
|
|
3475
|
+
ServiceID.NAVIGATION.value -> handleNavigationResponse(payload)
|
|
3476
|
+
ServiceID.EVEN_AI.value -> handleEvenAIResponse(payload)
|
|
3418
3477
|
ServiceID.EVEN_HUB_CTRL.value -> handleEvenHubCtrlResponse(payload)
|
|
3419
3478
|
else -> {
|
|
3420
3479
|
Bridge.log(
|
|
@@ -3426,6 +3485,69 @@ class G2 : SGCManager() {
|
|
|
3426
3485
|
}
|
|
3427
3486
|
}
|
|
3428
3487
|
|
|
3488
|
+
/**
|
|
3489
|
+
* EvenAI service (0x07). Logs the decoded EvenAIDataPackage so we can read the CONFIG
|
|
3490
|
+
* (Hey Even) echo: commandId=10 (CONFIG), config sub-message in field 13.
|
|
3491
|
+
*/
|
|
3492
|
+
private fun handleEvenAIResponse(payload: ByteArray) {
|
|
3493
|
+
val reader = ProtobufReader(payload)
|
|
3494
|
+
val fields = reader.parseFields()
|
|
3495
|
+
val cmd = fields[1] as? Int ?: -1
|
|
3496
|
+
val configData = fields[13] as? ByteArray
|
|
3497
|
+
if (cmd == 10 && configData != null) {
|
|
3498
|
+
val cReader = ProtobufReader(configData)
|
|
3499
|
+
val cFields = cReader.parseFields()
|
|
3500
|
+
val voiceSwitch = cFields[1] as? Int ?: 0 // omitted = 0 = OFF
|
|
3501
|
+
Bridge.log(
|
|
3502
|
+
"G2: EvenAI CONFIG echo — voiceSwitch=$voiceSwitch (${if (voiceSwitch == 1) "ON" else "OFF"}) config=$cFields"
|
|
3503
|
+
)
|
|
3504
|
+
} else {
|
|
3505
|
+
Bridge.log(
|
|
3506
|
+
"G2: EvenAI cmd=$cmd fields=${fields.keys.sorted()} raw=${payload.joinToString("") { String.format("%02X", it) }}"
|
|
3507
|
+
)
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
/**
|
|
3512
|
+
* Navigation service (0x08).
|
|
3513
|
+
*
|
|
3514
|
+
* OS_NOTIFY_COMPASS_CHANGED (15) carries the magnetometer heading in compass_info_msg
|
|
3515
|
+
* (field 10) → field 1, as whole degrees 0…359. (The proto names that field `compassIndex`,
|
|
3516
|
+
* but on the notify path it's the live heading — verified on-device: values sweep 0–359 as
|
|
3517
|
+
* the wearer turns.)
|
|
3518
|
+
*/
|
|
3519
|
+
private fun handleNavigationResponse(payload: ByteArray) {
|
|
3520
|
+
val reader = ProtobufReader(payload)
|
|
3521
|
+
val fields = reader.parseFields()
|
|
3522
|
+
val cmd = fields[1] as? Int ?: return
|
|
3523
|
+
|
|
3524
|
+
when (cmd) {
|
|
3525
|
+
NavigationCmd.OS_NOTIFY_COMPASS_CHANGED.value -> {
|
|
3526
|
+
val compassData = fields[10] as? ByteArray ?: return
|
|
3527
|
+
val cReader = ProtobufReader(compassData)
|
|
3528
|
+
val cFields = cReader.parseFields()
|
|
3529
|
+
val heading = cFields[1] as? Int ?: return
|
|
3530
|
+
// Heading in degrees, 0…359.
|
|
3531
|
+
Bridge.log("G2: compass heading=$heading°")
|
|
3532
|
+
Bridge.sendTypedMessage(
|
|
3533
|
+
"CompassHeadingEvent",
|
|
3534
|
+
mapOf(
|
|
3535
|
+
"heading" to heading,
|
|
3536
|
+
"timestamp" to System.currentTimeMillis()
|
|
3537
|
+
)
|
|
3538
|
+
)
|
|
3539
|
+
}
|
|
3540
|
+
NavigationCmd.OS_NOTIFY_COMPASS_CALIBRATE_START.value -> {
|
|
3541
|
+
Bridge.log("G2: compass calibration started — wearer should look around")
|
|
3542
|
+
Bridge.sendTypedMessage("CompassCalibrationEvent", mapOf("status" to "start"))
|
|
3543
|
+
}
|
|
3544
|
+
NavigationCmd.OS_NOTIFY_COMPASS_CALIBRATE_COMPLETE.value -> {
|
|
3545
|
+
Bridge.log("G2: compass calibration complete")
|
|
3546
|
+
Bridge.sendTypedMessage("CompassCalibrationEvent", mapOf("status" to "complete"))
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3429
3551
|
private fun handleEvenHubResponse(payload: ByteArray) {
|
|
3430
3552
|
val reader = ProtobufReader(payload)
|
|
3431
3553
|
val fields = reader.parseFields()
|
|
@@ -3476,7 +3598,22 @@ class G2 : SGCManager() {
|
|
|
3476
3598
|
Bridge.log("G2: Menu selection ignored — placeholder or unknown appId=$appId")
|
|
3477
3599
|
}
|
|
3478
3600
|
} else {
|
|
3479
|
-
//
|
|
3601
|
+
// Dedup only the non-critical logging path (img-success/error chatter), which L and R
|
|
3602
|
+
// both deliver. Page-state resets above are intentionally outside this window.
|
|
3603
|
+
val timestamp = System.currentTimeMillis()
|
|
3604
|
+
val lastResponse = lastEvenHubResponseTimestamp
|
|
3605
|
+
if (lastResponse != null && timestamp - lastResponse < 100) {
|
|
3606
|
+
return
|
|
3607
|
+
}
|
|
3608
|
+
lastEvenHubResponseTimestamp = timestamp
|
|
3609
|
+
|
|
3610
|
+
// If glasses sent a shutdown (cmd=9/10), our page is gone — reset state.
|
|
3611
|
+
if (cmdValue == 9 || cmdValue == 10) {
|
|
3612
|
+
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
3613
|
+
pageCreated = false
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
// Scan response fields for a shutdown/error code regardless of the debounce window.
|
|
3480
3617
|
// field 4 = StartupResCmd, field 6 = ImgResCmd, field 8 = RebuildResCmd, field 10 =
|
|
3481
3618
|
// TextResCmd
|
|
3482
3619
|
for (resField in listOf(4, 6, 8, 10)) {
|
|
@@ -3490,12 +3627,15 @@ class G2 : SGCManager() {
|
|
|
3490
3627
|
Bridge.log(
|
|
3491
3628
|
"G2: WARN: Glasses shutdown our EvenHub page — resetting page state"
|
|
3492
3629
|
)
|
|
3493
|
-
startupPageCreated = false
|
|
3494
3630
|
pageCreated = false
|
|
3495
|
-
pageHasTextContainer = false
|
|
3496
|
-
currentTextContent = ""
|
|
3497
3631
|
}
|
|
3498
3632
|
}
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
for (resField in listOf(4, 6, 8, 10)) {
|
|
3636
|
+
val resData = fields[resField] as? ByteArray ?: continue
|
|
3637
|
+
val resReader = ProtobufReader(resData)
|
|
3638
|
+
val resFields = resReader.parseFields()
|
|
3499
3639
|
(resFields[8] as? Int)?.let { errorCode ->
|
|
3500
3640
|
// ImgResCmd has ErrorCode in field 8
|
|
3501
3641
|
if (errorCode == 4) {
|
|
@@ -3505,15 +3645,6 @@ class G2 : SGCManager() {
|
|
|
3505
3645
|
}
|
|
3506
3646
|
}
|
|
3507
3647
|
}
|
|
3508
|
-
|
|
3509
|
-
// If glasses sent a shutdown (cmd=9/10), our page is gone — reset state
|
|
3510
|
-
if (cmdValue == 9 || cmdValue == 10) {
|
|
3511
|
-
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
3512
|
-
startupPageCreated = false
|
|
3513
|
-
pageCreated = false
|
|
3514
|
-
pageHasTextContainer = false
|
|
3515
|
-
currentTextContent = ""
|
|
3516
|
-
}
|
|
3517
3648
|
}
|
|
3518
3649
|
}
|
|
3519
3650
|
|
|
@@ -3541,6 +3672,39 @@ class G2 : SGCManager() {
|
|
|
3541
3672
|
}
|
|
3542
3673
|
}
|
|
3543
3674
|
|
|
3675
|
+
/**
|
|
3676
|
+
* Parse an IMU_Report_Data sub-message: fields 1/2/3 = x/y/z as 32-bit floats (wire type 5).
|
|
3677
|
+
* `ProtobufReader.parseFields()` skips wire-type-5 fields, so this walks the bytes manually.
|
|
3678
|
+
*/
|
|
3679
|
+
private fun parseImuReportData(data: ByteArray): Triple<Float, Float, Float>? {
|
|
3680
|
+
var x: Float? = null
|
|
3681
|
+
var y: Float? = null
|
|
3682
|
+
var z: Float? = null
|
|
3683
|
+
var i = 0
|
|
3684
|
+
while (i < data.size) {
|
|
3685
|
+
val tag = data[i].toInt() and 0xFF
|
|
3686
|
+
i += 1
|
|
3687
|
+
val fieldNum = tag shr 3
|
|
3688
|
+
val wireType = tag and 0x07
|
|
3689
|
+
if (wireType != 5 || data.size - i < 4) break
|
|
3690
|
+
var bits = 0
|
|
3691
|
+
for (b in 0 until 4) {
|
|
3692
|
+
bits = bits or ((data[i + b].toInt() and 0xFF) shl (8 * b)) // little-endian
|
|
3693
|
+
}
|
|
3694
|
+
i += 4
|
|
3695
|
+
val value = Float.fromBits(bits)
|
|
3696
|
+
when (fieldNum) {
|
|
3697
|
+
1 -> x = value
|
|
3698
|
+
2 -> y = value
|
|
3699
|
+
3 -> z = value
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
val fx = x ?: return null
|
|
3703
|
+
val fy = y ?: return null
|
|
3704
|
+
val fz = z ?: return null
|
|
3705
|
+
return Triple(fx, fy, fz)
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3544
3708
|
private fun handleTouchEvent(devEventData: ByteArray) {
|
|
3545
3709
|
// Parse SendDeviceEvent: field 1=ListEvent, field 2=TextEvent, field 3=SysEvent
|
|
3546
3710
|
val reader = ProtobufReader(devEventData)
|
|
@@ -3556,6 +3720,19 @@ class G2 : SGCManager() {
|
|
|
3556
3720
|
val sysReader = ProtobufReader(sysData)
|
|
3557
3721
|
val sysFields = sysReader.parseFields()
|
|
3558
3722
|
|
|
3723
|
+
// IMU data report: eventType == IMU_DATA_REPORT (8), imuData in field 3
|
|
3724
|
+
// (IMU_Report_Data { x, y, z } as 32-bit floats). Handle and return before the
|
|
3725
|
+
// gesture-mapping path.
|
|
3726
|
+
val imuData = sysFields[3] as? ByteArray
|
|
3727
|
+
if ((sysFields[1] as? Int) == OsEventType.IMU_DATA_REPORT.value && imuData != null) {
|
|
3728
|
+
val imu = parseImuReportData(imuData)
|
|
3729
|
+
if (imu != null) {
|
|
3730
|
+
Bridge.log("G2: IMU data report: ${imu.first}, ${imu.second}, ${imu.third}")
|
|
3731
|
+
Bridge.sendAccelEvent(imu.first, imu.second, imu.third, timestamp)
|
|
3732
|
+
return
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3559
3736
|
val normalType = sysFields[1] as? Int
|
|
3560
3737
|
val eventType: OsEventType? =
|
|
3561
3738
|
if (normalType != null) OsEventType.fromInt(normalType) else OsEventType.CLICK
|
|
@@ -3596,11 +3773,7 @@ class G2 : SGCManager() {
|
|
|
3596
3773
|
// System exit: glasses killed our EvenHub page (user opened menu or another app)
|
|
3597
3774
|
// Reset page state and re-create the page to reclaim EvenHub focus
|
|
3598
3775
|
if (eventType == OsEventType.SYSTEM_EXIT || eventType == OsEventType.ABNORMAL_EXIT) {
|
|
3599
|
-
startupPageCreated = false
|
|
3600
3776
|
pageCreated = false
|
|
3601
|
-
pageHasTextContainer = false
|
|
3602
|
-
currentTextContent = ""
|
|
3603
|
-
currentBitmapBase64 = ""
|
|
3604
3777
|
// Firmware kills the mic on system exit; re-arm it if it should be on
|
|
3605
3778
|
DeviceStore.apply("glasses", "micEnabled", false)
|
|
3606
3779
|
DeviceManager.getInstance().updateMicState()
|
|
@@ -3641,7 +3814,8 @@ class G2 : SGCManager() {
|
|
|
3641
3814
|
OsEventType.FOREGROUND_ENTER -> "foreground_enter"
|
|
3642
3815
|
OsEventType.FOREGROUND_EXIT -> "foreground_exit"
|
|
3643
3816
|
OsEventType.SYSTEM_EXIT -> "system_exit"
|
|
3644
|
-
OsEventType.
|
|
3817
|
+
OsEventType.IMU_DATA_REPORT -> null
|
|
3818
|
+
OsEventType.ABNORMAL_EXIT -> null // don't report abnormal exits as gestures
|
|
3645
3819
|
}
|
|
3646
3820
|
}
|
|
3647
3821
|
|
|
@@ -3653,9 +3827,9 @@ class G2 : SGCManager() {
|
|
|
3653
3827
|
// Ignore heartbeat acks
|
|
3654
3828
|
if (cmdValue == DevCfgCommandId.BASE_CONN_HEART_BEAT.value) return
|
|
3655
3829
|
|
|
3656
|
-
Bridge.log(
|
|
3657
|
-
|
|
3658
|
-
)
|
|
3830
|
+
// Bridge.log(
|
|
3831
|
+
// "G2: DevSettings response: ${payload.take(32).joinToString(":") { String.format("%02X", it) }}"
|
|
3832
|
+
// )
|
|
3659
3833
|
|
|
3660
3834
|
if (cmdValue == DevCfgCommandId.AUTHENTICATION.value) {
|
|
3661
3835
|
// DevCfgDataPackage: field 2 = magicRandom, field 3 = AuthMgr { field 1 = secAuth }
|
|
@@ -3697,7 +3871,7 @@ class G2 : SGCManager() {
|
|
|
3697
3871
|
}
|
|
3698
3872
|
|
|
3699
3873
|
val connStatus = ringFields[4] as? Int ?: -1
|
|
3700
|
-
Bridge.log("G2: Ring connection status: connStatus?=$connStatus")
|
|
3874
|
+
// Bridge.log("G2: Ring connection status: connStatus?=$connStatus")
|
|
3701
3875
|
|
|
3702
3876
|
if (connStatus == 22) {
|
|
3703
3877
|
Bridge.log("G2: Ring disconnected")
|
|
@@ -3765,7 +3939,7 @@ class G2 : SGCManager() {
|
|
|
3765
3939
|
val timestamp = System.currentTimeMillis()
|
|
3766
3940
|
val last = lastGestureCtrlTimestamp
|
|
3767
3941
|
if (last != null && timestamp - last < 500) {
|
|
3768
|
-
Bridge.log("G2: gesture_ctrl dedup")
|
|
3942
|
+
// Bridge.log("G2: gesture_ctrl dedup")
|
|
3769
3943
|
return
|
|
3770
3944
|
}
|
|
3771
3945
|
lastGestureCtrlTimestamp = timestamp
|
|
@@ -3775,24 +3949,36 @@ class G2 : SGCManager() {
|
|
|
3775
3949
|
Bridge.log("G2: dashboard closed / shutdown - dashboardShowing=$dashboardShowing")
|
|
3776
3950
|
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
3777
3951
|
if (!useNativeDashboard) {
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
//
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3952
|
+
dashboardShowing = 0
|
|
3953
|
+
// Rebuild the page from cached containers, then reconcile against
|
|
3954
|
+
// DeviceManager's authoritative current view so the glasses match the phone
|
|
3955
|
+
// (not just the last-cached G2 containers) after returning from the dashboard.
|
|
3956
|
+
displayScope.launch {
|
|
3957
|
+
displayMutex.withLock { rebuildState() }
|
|
3958
|
+
DeviceManager.getInstance().sendCurrentState()
|
|
3959
|
+
// set the mic back on if it should be on — only after the rebuild
|
|
3960
|
+
// completes, matching iOS (which awaits rebuildState before restartMic).
|
|
3961
|
+
val micEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
3962
|
+
if (micEnabled) {
|
|
3963
|
+
restartMic()
|
|
3964
|
+
}
|
|
3784
3965
|
}
|
|
3966
|
+
return
|
|
3785
3967
|
} else {
|
|
3786
3968
|
// if we aren't trying to show the dashboard
|
|
3787
3969
|
// then we need to turn the mic back on and display the mentra main page:
|
|
3788
3970
|
if (dashboardShowing <= 1) {
|
|
3789
3971
|
dashboardShowing = 0
|
|
3790
|
-
//
|
|
3791
|
-
DeviceManager
|
|
3792
|
-
//
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3972
|
+
// Rebuild the page from cached containers, then reconcile against
|
|
3973
|
+
// DeviceManager's authoritative current view so the glasses match the phone
|
|
3974
|
+
// (not just the last-cached G2 containers) after returning from the dashboard.
|
|
3975
|
+
displayScope.launch {
|
|
3976
|
+
displayMutex.withLock { rebuildState() }
|
|
3977
|
+
// set the mic back on if it should be on
|
|
3978
|
+
val micEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
3979
|
+
if (micEnabled) {
|
|
3980
|
+
restartMic()
|
|
3981
|
+
}
|
|
3796
3982
|
}
|
|
3797
3983
|
return
|
|
3798
3984
|
}
|
|
@@ -3828,7 +4014,7 @@ class G2 : SGCManager() {
|
|
|
3828
4014
|
// Battery
|
|
3829
4015
|
(fields[12] as? Int)?.let { battery ->
|
|
3830
4016
|
if (battery in 0..100) {
|
|
3831
|
-
Bridge.log("G2: Battery level: $battery%")
|
|
4017
|
+
// Bridge.log("G2: Battery level: $battery%")
|
|
3832
4018
|
batteryLevel_ = battery
|
|
3833
4019
|
}
|
|
3834
4020
|
}
|
|
@@ -3845,13 +4031,13 @@ class G2 : SGCManager() {
|
|
|
3845
4031
|
// Software versions
|
|
3846
4032
|
(fields[5] as? ByteArray)?.let { leftVer ->
|
|
3847
4033
|
val leftVersion = String(leftVer, Charsets.UTF_8)
|
|
3848
|
-
Bridge.log("G2: Left firmware: $leftVersion")
|
|
4034
|
+
// Bridge.log("G2: Left firmware: $leftVersion")
|
|
3849
4035
|
DeviceStore.apply("glasses", "leftFirmwareVersion", leftVersion)
|
|
3850
4036
|
}
|
|
3851
4037
|
|
|
3852
4038
|
(fields[6] as? ByteArray)?.let { rightVer ->
|
|
3853
4039
|
val rightVersion = String(rightVer, Charsets.UTF_8)
|
|
3854
|
-
Bridge.log("G2: Right firmware: $rightVersion")
|
|
4040
|
+
// Bridge.log("G2: Right firmware: $rightVersion")
|
|
3855
4041
|
DeviceStore.apply("glasses", "rightFirmwareVersion", rightVersion)
|
|
3856
4042
|
DeviceStore.apply("glasses", "firmwareVersion", rightVersion)
|
|
3857
4043
|
}
|
|
@@ -3880,7 +4066,7 @@ class G2 : SGCManager() {
|
|
|
3880
4066
|
return
|
|
3881
4067
|
}
|
|
3882
4068
|
lastAudioFrame = audioData
|
|
3883
|
-
Bridge.log("G2: audio data from $sourceKey: ${data.take(10).joinToString("") { String.format("%02X", it) }}")
|
|
4069
|
+
// Bridge.log("G2: audio data from $sourceKey: ${data.take(10).joinToString("") { String.format("%02X", it) }}")
|
|
3884
4070
|
DeviceManager.getInstance().handleGlassesMicData(audioData, 40)
|
|
3885
4071
|
}
|
|
3886
4072
|
|