@glassly/bluetooth-sdk 0.3.1-dev.535 → 0.3.1-dev.537
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/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +13 -9
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +10 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +2 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +105 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +49 -32
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +13 -5
- package/ios/Source/DeviceStore.swift +7 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +3 -3
- package/ios/Source/controllers/ControllerManager.swift +2 -0
- package/ios/Source/controllers/R1.swift +105 -22
- package/ios/Source/sgcs/G2.swift +50 -39
- package/ios/Source/sgcs/SGCManager.swift +5 -0
- package/package.json +1 -1
- package/src/generated/releaseMetadata.ts +3 -3
|
@@ -2340,11 +2340,22 @@ class DeviceManager {
|
|
|
2340
2340
|
Bridge.log("MAN: connectDefaultController skipped — bluetooth runtime permissions not granted")
|
|
2341
2341
|
return
|
|
2342
2342
|
}
|
|
2343
|
+
// The ring link outlives glasses connects/disconnects, so every
|
|
2344
|
+
// reconnect path may find it already up.
|
|
2345
|
+
if (DeviceStore.get("glasses", "controllerConnected") as? Boolean == true) {
|
|
2346
|
+
Bridge.log("MAN: Controller already connected, returning")
|
|
2347
|
+
return
|
|
2348
|
+
}
|
|
2343
2349
|
initController(defaultController)
|
|
2344
2350
|
searchingController = true
|
|
2345
2351
|
controller?.connectById(controllerDeviceName)
|
|
2346
2352
|
}
|
|
2347
2353
|
|
|
2354
|
+
/** Debug `ring_via_glasses` flipped: re-route a connected ring live. */
|
|
2355
|
+
fun ringRouteToggled(viaGlasses: Boolean) {
|
|
2356
|
+
controller?.setRouteViaGlasses(viaGlasses)
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2348
2359
|
fun connectByName(dName: String) {
|
|
2349
2360
|
Bridge.log("MAN: Connecting to wearable: $dName")
|
|
2350
2361
|
|
|
@@ -2435,11 +2446,8 @@ class DeviceManager {
|
|
|
2435
2446
|
"voiceActivityDetectionEnabled",
|
|
2436
2447
|
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
|
|
2437
2448
|
)
|
|
2438
|
-
//
|
|
2439
|
-
|
|
2440
|
-
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
2441
|
-
controller?.disconnect()
|
|
2442
|
-
controller = null
|
|
2449
|
+
// The ring is its own link and stays up across glasses disconnects;
|
|
2450
|
+
// disconnectController()/forgetController() are the explicit ways down.
|
|
2443
2451
|
if (clearPendingIdentity) {
|
|
2444
2452
|
clearPendingConnection()
|
|
2445
2453
|
}
|
|
@@ -2489,10 +2497,6 @@ class DeviceManager {
|
|
|
2489
2497
|
"voiceActivityDetectionEnabled",
|
|
2490
2498
|
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED,
|
|
2491
2499
|
)
|
|
2492
|
-
searchingController = false
|
|
2493
|
-
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
2494
|
-
controller?.disconnect()
|
|
2495
|
-
controller = null
|
|
2496
2500
|
} else {
|
|
2497
2501
|
// Typical abandonAttempt path: disconnect() already destroyed SGC and nulled
|
|
2498
2502
|
// this.sgc, so MentraLive.forget() never ran and Classic bonds stayed up —
|
|
@@ -148,6 +148,8 @@ object DeviceStore {
|
|
|
148
148
|
store.set("bluetooth", "cfw_fast_ble", false)
|
|
149
149
|
// Debug: G2PERF display performance lines in the log (off = counted, not logged).
|
|
150
150
|
store.set("bluetooth", "g2_perf_logs", false)
|
|
151
|
+
// Debug: link the R1 ring to the glasses (off = ring on the phone link only).
|
|
152
|
+
store.set("bluetooth", "ring_via_glasses", false)
|
|
151
153
|
store.set("bluetooth", "button_photo_size", "max")
|
|
152
154
|
store.set("bluetooth", "button_max_recording_time", 10)
|
|
153
155
|
store.set("bluetooth", "camera_fov", mapOf("fov" to 118, "roi_position" to 0))
|
|
@@ -264,6 +266,14 @@ object DeviceStore {
|
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
268
|
}
|
|
269
|
+
"bluetooth" to "ring_via_glasses" -> {
|
|
270
|
+
val viaGlasses = value as? Boolean
|
|
271
|
+
if (viaGlasses != null && (oldValue as? Boolean) != viaGlasses) {
|
|
272
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
273
|
+
DeviceManager.getInstance().ringRouteToggled(viaGlasses)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
267
277
|
"bluetooth" to "display_max_fps" -> {
|
|
268
278
|
(value as? Number)?.let { fps ->
|
|
269
279
|
DeviceManager.getInstance().setDisplayMaxFps(fps.toInt())
|
|
@@ -3,9 +3,9 @@ package com.glassly.bluetoothsdk
|
|
|
3
3
|
/** Generated by release CI. Do not edit in a release checkout. */
|
|
4
4
|
internal object GeneratedReleaseMetadata {
|
|
5
5
|
const val FAMILY_BASE_VERSION: String = "0.3.1"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "0.3.1-dev.
|
|
7
|
-
const val RELEASE_SET_ID: String = "glassly-0.3.1-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "0.3.1-dev.537"
|
|
7
|
+
const val RELEASE_SET_ID: String = "glassly-0.3.1-dev.537"
|
|
8
|
+
const val SOURCE_COMMIT: String = "2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a"
|
|
9
9
|
const val OTA_MANIFEST_URL: String = ""
|
|
10
10
|
const val OTA_MANIFEST_SHA256: String = ""
|
|
11
11
|
}
|
|
@@ -63,6 +63,8 @@ abstract class ControllerManager {
|
|
|
63
63
|
// Connection Management
|
|
64
64
|
abstract fun disconnect()
|
|
65
65
|
abstract fun forget()
|
|
66
|
+
/** Route input through the glasses (debug) or the phone link (default). */
|
|
67
|
+
open fun setRouteViaGlasses(viaGlasses: Boolean) {}
|
|
66
68
|
abstract fun findCompatibleDevices()
|
|
67
69
|
abstract fun stopScan()
|
|
68
70
|
abstract fun connectById(id: String)
|
|
@@ -45,6 +45,45 @@ private object R1BLE {
|
|
|
45
45
|
val NAME_FILTERS = arrayOf("EVEN R1", "BCL60")
|
|
46
46
|
|
|
47
47
|
const val GESTURE_MARKER: Byte = 0xFF.toByte()
|
|
48
|
+
|
|
49
|
+
/** `source` on emitted touch events; the same value the G2 CFW tags
|
|
50
|
+
* ring input it relays with, so JS treats both paths alike. */
|
|
51
|
+
const val RING_SOURCE = 2
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - Ring → phone touch report (channel 1)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The ring reports touch on the legacy channel-1 transport as 11 bytes
|
|
58
|
+
* `[00][09][61][00][wireType][aux][speed][00][tick LE32]` — the same report
|
|
59
|
+
* the Glassly CFW relays from the glasses as the `RI` SysEvent extension
|
|
60
|
+
* (g2flash gesture_fwd.c `faceclaw_ring_report`). Wire types map onto the
|
|
61
|
+
* gesture names the G2 path emits so miniapps see one vocabulary.
|
|
62
|
+
*/
|
|
63
|
+
private object R1TouchReport {
|
|
64
|
+
const val LENGTH = 11
|
|
65
|
+
const val OPCODE: Byte = 0x61
|
|
66
|
+
|
|
67
|
+
fun gestureName(wireType: Int): String? = when (wireType) {
|
|
68
|
+
0 -> "tap_start"
|
|
69
|
+
1 -> "single_tap"
|
|
70
|
+
2 -> "double_tap"
|
|
71
|
+
4 -> "swipe_up"
|
|
72
|
+
5 -> "swipe_down"
|
|
73
|
+
8 -> "tap_end"
|
|
74
|
+
9 -> "tap_then_hold"
|
|
75
|
+
10 -> "touch_down"
|
|
76
|
+
else -> null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** null when `data` is not a touch report; `.first` null for an unknown wire type. */
|
|
80
|
+
fun parse(data: ByteArray): Pair<String?, Int>? {
|
|
81
|
+
if (data.size != LENGTH || data[0] != 0.toByte() || data[1] != 9.toByte() ||
|
|
82
|
+
data[2] != OPCODE || data[3] != 0.toByte()
|
|
83
|
+
) return null
|
|
84
|
+
val wireType = data[4].toInt() and 0xFF
|
|
85
|
+
return gestureName(wireType) to wireType
|
|
86
|
+
}
|
|
48
87
|
}
|
|
49
88
|
|
|
50
89
|
// MARK: - EUS transport encoder (phone → ring, channel 2)
|
|
@@ -75,6 +114,12 @@ private object R1Eus {
|
|
|
75
114
|
/** system subcommand 0x0A: 12-byte payload = two 6-byte glasses arm MACs;
|
|
76
115
|
* the ring stores both targets and fast-advertises for them */
|
|
77
116
|
const val SUBCMD_ADV_START = 0x0A
|
|
117
|
+
/** system subcommand 0x82 (removeRingNotify): one-byte SET erases both
|
|
118
|
+
* stored glasses targets and the "glasses paired" flag. While the ring
|
|
119
|
+
* holds glasses targets its touch reports go to the glasses and the
|
|
120
|
+
* phone link carries telemetry only, so this is what puts input on the
|
|
121
|
+
* phone link. */
|
|
122
|
+
const val SUBCMD_REMOVE_RING = 0x82
|
|
78
123
|
|
|
79
124
|
private fun crc16Step(crc: Int, byte: Int): Int {
|
|
80
125
|
var c = ((crc shr 8) or (crc shl 8)) and 0xFFFF
|
|
@@ -144,7 +189,7 @@ private object R1Eus {
|
|
|
144
189
|
// MARK: - R1 Gesture Types
|
|
145
190
|
|
|
146
191
|
private enum class R1Gesture(val rawValue: String) {
|
|
147
|
-
HOLD("
|
|
192
|
+
HOLD("tap_then_hold"),
|
|
148
193
|
SINGLE_TAP("single_tap"),
|
|
149
194
|
DOUBLE_TAP("double_tap"),
|
|
150
195
|
SWIPE_UP("swipe_up"),
|
|
@@ -554,20 +599,58 @@ class R1 : ControllerManager() {
|
|
|
554
599
|
DeviceStore.apply("glasses", "controllerMacAddress", mac)
|
|
555
600
|
DeviceStore.apply("glasses", "controllerConnected", true)
|
|
556
601
|
// The phone link owns these flags: a ready phone link IS pairing
|
|
557
|
-
// success (fires handleControllerReady).
|
|
558
|
-
// on afterwards
|
|
602
|
+
// success (fires handleControllerReady). A glasses link is only
|
|
603
|
+
// layered on afterwards (advStart + RING_CONNECT_INFO) when debug-routed.
|
|
559
604
|
DeviceStore.apply("glasses", "controllerFullyBooted", true)
|
|
560
605
|
|
|
561
|
-
|
|
562
|
-
|
|
606
|
+
applyRoute(routeViaGlasses)
|
|
607
|
+
|
|
608
|
+
startHeartbeat()
|
|
609
|
+
}
|
|
563
610
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
611
|
+
/** Debug `ring_via_glasses` (default off): link the ring to the glasses and
|
|
612
|
+
* take its input through the CFW relay instead of this link. */
|
|
613
|
+
private val routeViaGlasses: Boolean
|
|
614
|
+
get() = DeviceStore.get("bluetooth", "ring_via_glasses") as? Boolean ?: false
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Phone route (default): erase the ring's stored glasses targets so its
|
|
618
|
+
* touch reports come to this link, and have the glasses drop any ring
|
|
619
|
+
* link they still hold. Glasses route (debug): hand the ring the glasses
|
|
620
|
+
* MACs, then ask the glasses to connect.
|
|
621
|
+
*/
|
|
622
|
+
private fun applyRoute(viaGlasses: Boolean) {
|
|
623
|
+
if (viaGlasses) {
|
|
624
|
+
connectToGlasses()
|
|
625
|
+
// after a second, connect the glasses to the controller if needed:
|
|
626
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
627
|
+
delay(1000)
|
|
628
|
+
DeviceManager.getInstance().sgc?.connectController()
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
sendEusCommand(R1Eus.SUBCMD_REMOVE_RING, byteArrayOf(0x01), "removeRingNotify")
|
|
632
|
+
DeviceManager.getInstance().sgc?.releaseController()
|
|
568
633
|
}
|
|
634
|
+
}
|
|
569
635
|
|
|
570
|
-
|
|
636
|
+
override fun setRouteViaGlasses(viaGlasses: Boolean) {
|
|
637
|
+
Bridge.log("R1: route via ${if (viaGlasses) "glasses" else "phone"}")
|
|
638
|
+
if (DeviceStore.get("glasses", "controllerConnected") as? Boolean != true) return
|
|
639
|
+
applyRoute(viaGlasses)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* One emit path for both direct parsers: dropped while the ring is
|
|
644
|
+
* debug-routed via the glasses so a lingering phone-side report can't
|
|
645
|
+
* double up with the CFW relay.
|
|
646
|
+
*/
|
|
647
|
+
private fun emitGesture(gestureName: String, timestamp: Long, via: String) {
|
|
648
|
+
if (routeViaGlasses) {
|
|
649
|
+
Bridge.log("R1: $via $gestureName ignored (ring routed via glasses)")
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
Bridge.log("R1: $via: $gestureName")
|
|
653
|
+
Bridge.sendTouchEvent(ControllerTypes.R1, gestureName, timestamp, R1BLE.RING_SOURCE)
|
|
571
654
|
}
|
|
572
655
|
|
|
573
656
|
/**
|
|
@@ -657,12 +740,21 @@ class R1 : ControllerManager() {
|
|
|
657
740
|
|
|
658
741
|
val timestamp = System.currentTimeMillis()
|
|
659
742
|
|
|
743
|
+
// Touch report: [00 09 61 00 wireType aux speed 00 tick4]
|
|
744
|
+
R1TouchReport.parse(data)?.let { (gesture, wireType) ->
|
|
745
|
+
if (gesture != null) {
|
|
746
|
+
emitGesture(gesture, timestamp, "Touch report")
|
|
747
|
+
} else {
|
|
748
|
+
Bridge.log("R1: Unknown touch report wireType=$wireType")
|
|
749
|
+
}
|
|
750
|
+
return
|
|
751
|
+
}
|
|
752
|
+
|
|
660
753
|
// Gesture: [0xFF, type, param]
|
|
661
754
|
if (data.size >= 3 && data[0] == R1BLE.GESTURE_MARKER) {
|
|
662
755
|
val gesture = R1Gesture.parse(data)
|
|
663
756
|
if (gesture != null) {
|
|
664
|
-
|
|
665
|
-
Bridge.sendTouchEvent(ControllerTypes.R1, gesture.rawValue, timestamp)
|
|
757
|
+
emitGesture(gesture.rawValue, timestamp, "Gesture")
|
|
666
758
|
} else {
|
|
667
759
|
Bridge.log(
|
|
668
760
|
"R1: Unknown gesture type=0x%02X param=0x%02X".format(data[1], data[2])
|
|
@@ -696,8 +788,7 @@ class R1 : ControllerManager() {
|
|
|
696
788
|
val slice = byteArrayOf(data[ffIndex], data[ffIndex + 1], data[ffIndex + 2])
|
|
697
789
|
val gesture = R1Gesture.parse(slice)
|
|
698
790
|
if (gesture != null) {
|
|
699
|
-
|
|
700
|
-
Bridge.sendTouchEvent(ControllerTypes.R1, gesture.rawValue, timestamp)
|
|
791
|
+
emitGesture(gesture.rawValue, timestamp, "Embedded gesture")
|
|
701
792
|
return
|
|
702
793
|
}
|
|
703
794
|
}
|
|
@@ -2298,7 +2298,7 @@ class G2 : SGCManager() {
|
|
|
2298
2298
|
|
|
2299
2299
|
setFullyConnected()
|
|
2300
2300
|
|
|
2301
|
-
//
|
|
2301
|
+
// Link a paired ring if it is routed via the glasses, else release it
|
|
2302
2302
|
connectController()
|
|
2303
2303
|
|
|
2304
2304
|
// Query version + battery info from glasses
|
|
@@ -5596,52 +5596,57 @@ class G2 : SGCManager() {
|
|
|
5596
5596
|
connectController()
|
|
5597
5597
|
}
|
|
5598
5598
|
|
|
5599
|
-
|
|
5599
|
+
/**
|
|
5600
|
+
* Debug `ring_via_glasses`: the ring is linked to the glasses and its
|
|
5601
|
+
* input arrives through the CFW. Off (default), the ring stays on the
|
|
5602
|
+
* phone link and the glasses must not hold it — the ring sends touch
|
|
5603
|
+
* reports to whichever glasses link it has and only telemetry to the phone.
|
|
5604
|
+
*/
|
|
5605
|
+
private val ringViaGlasses: Boolean
|
|
5606
|
+
get() = DeviceStore.get("bluetooth", "ring_via_glasses") as? Boolean ?: false
|
|
5607
|
+
|
|
5608
|
+
/** RING_CONNECT_INFO for the paired ring. False when the glasses aren't
|
|
5609
|
+
* booted or no ring MAC is known. */
|
|
5610
|
+
private fun sendRingConnectInfo(connect: Boolean, tag: String): Boolean {
|
|
5600
5611
|
val isFullyBooted = DeviceStore.get("glasses", "fullyBooted") as? Boolean ?: false
|
|
5601
5612
|
if (!isFullyBooted) {
|
|
5602
|
-
Bridge.log("G2:
|
|
5603
|
-
return
|
|
5613
|
+
Bridge.log("G2: $tag - g2 not fully booted, ignoring")
|
|
5614
|
+
return false
|
|
5604
5615
|
}
|
|
5605
5616
|
val mac = DeviceStore.get("glasses", "controllerMacAddress") as? String
|
|
5606
5617
|
if (mac.isNullOrEmpty()) {
|
|
5607
|
-
Bridge.log("G2:
|
|
5608
|
-
return
|
|
5618
|
+
Bridge.log("G2: $tag - no MAC address found")
|
|
5619
|
+
return false
|
|
5609
5620
|
}
|
|
5610
5621
|
val hexParts = mac.split(":").mapNotNull { it.toIntOrNull(16)?.toByte() }
|
|
5611
5622
|
if (hexParts.size != 6) {
|
|
5612
|
-
Bridge.log("G2:
|
|
5613
|
-
return
|
|
5623
|
+
Bridge.log("G2: $tag - invalid MAC format: $mac")
|
|
5624
|
+
return false
|
|
5614
5625
|
}
|
|
5615
|
-
|
|
5616
|
-
val macData = hexParts.toByteArray()
|
|
5617
|
-
val msg = DevSettingsProto.ringConnectInfo(sendManager.nextMagicRandom(), true, macData)
|
|
5626
|
+
val msg = DevSettingsProto.ringConnectInfo(sendManager.nextMagicRandom(), connect, hexParts.toByteArray())
|
|
5618
5627
|
sendDevSettingsCommand(msg)
|
|
5619
|
-
Bridge.log("G2: Sent
|
|
5628
|
+
Bridge.log("G2: Sent RING_${if (connect) "CONNECT" else "DISCONNECT"}_INFO for MAC $mac")
|
|
5629
|
+
return true
|
|
5620
5630
|
}
|
|
5621
5631
|
|
|
5622
|
-
override fun
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
return
|
|
5627
|
-
}
|
|
5628
|
-
val mac = DeviceStore.get("glasses", "controllerMacAddress") as? String
|
|
5629
|
-
if (mac.isNullOrEmpty()) {
|
|
5630
|
-
Bridge.log("G2: disconnectController - no MAC address found")
|
|
5631
|
-
return
|
|
5632
|
-
}
|
|
5633
|
-
val hexParts = mac.split(":").mapNotNull { it.toIntOrNull(16)?.toByte() }
|
|
5634
|
-
if (hexParts.size != 6) {
|
|
5635
|
-
Bridge.log("G2: disconnectController - invalid MAC format: $mac")
|
|
5632
|
+
override fun connectController() {
|
|
5633
|
+
if (!ringViaGlasses) {
|
|
5634
|
+
// Phone-routed ring: make sure the glasses let go of it instead.
|
|
5635
|
+
releaseController()
|
|
5636
5636
|
return
|
|
5637
5637
|
}
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5638
|
+
sendRingConnectInfo(true, "connectController")
|
|
5639
|
+
}
|
|
5640
|
+
|
|
5641
|
+
override fun disconnectController() {
|
|
5642
|
+
if (!sendRingConnectInfo(false, "disconnectController")) return
|
|
5641
5643
|
// DeviceStore.apply("glasses", "controllerMacAddress", "")
|
|
5642
5644
|
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
5643
5645
|
DeviceStore.apply("glasses", "controllerFullyBooted", false)
|
|
5644
|
-
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
override fun releaseController() {
|
|
5649
|
+
sendRingConnectInfo(false, "releaseController")
|
|
5645
5650
|
}
|
|
5646
5651
|
|
|
5647
5652
|
// ---------- SGCManager: Device Control ----------
|
|
@@ -6779,9 +6784,20 @@ class G2 : SGCManager() {
|
|
|
6779
6784
|
return@let
|
|
6780
6785
|
}
|
|
6781
6786
|
|
|
6787
|
+
// Ring input relayed by the glasses. Older CFWs left event 14's
|
|
6788
|
+
// source unspecified, so the id alone also marks the ring.
|
|
6789
|
+
val fromRing = eventSource == 2 || eventType == OsEventType.RING_TOUCH_DOWN
|
|
6790
|
+
if (fromRing && !ringViaGlasses) {
|
|
6791
|
+
// The phone link owns ring input; the glasses shouldn't hold
|
|
6792
|
+
// the ring at all, so this is a stale link — drop it again.
|
|
6793
|
+
Bridge.log("G2: ring $gestureName via glasses ignored (ring routed via phone)")
|
|
6794
|
+
releaseController()
|
|
6795
|
+
return@let
|
|
6796
|
+
}
|
|
6797
|
+
|
|
6782
6798
|
Bridge.sendTouchEvent(DeviceTypes.G2, gestureName, timestamp, eventSource)
|
|
6783
6799
|
|
|
6784
|
-
if (
|
|
6800
|
+
if (fromRing) {
|
|
6785
6801
|
// controller must be connected and fully booted:
|
|
6786
6802
|
setControllerFullyConnected()
|
|
6787
6803
|
}
|
|
@@ -6905,7 +6921,8 @@ class G2 : SGCManager() {
|
|
|
6905
6921
|
if (connStatus == 22) {
|
|
6906
6922
|
// The ring↔glasses link dropped. The phone↔ring link owns
|
|
6907
6923
|
// controllerConnected/FullyBooted, so don't clear them here
|
|
6908
|
-
// (that would flap pairing state) — just re-request the link
|
|
6924
|
+
// (that would flap pairing state) — just re-request the link
|
|
6925
|
+
// (a no-op while the ring is routed via the phone).
|
|
6909
6926
|
Bridge.log("G2: Ring-glasses link dropped, re-requesting")
|
|
6910
6927
|
reconnectController()
|
|
6911
6928
|
}
|
|
@@ -355,6 +355,8 @@ abstract class SGCManager {
|
|
|
355
355
|
// Controller bridging (default no-op — only G2 supports pairing with a ring controller)
|
|
356
356
|
open fun connectController() {}
|
|
357
357
|
open fun disconnectController() {}
|
|
358
|
+
/** Drop the glasses↔ring link without touching the phone↔ring state. */
|
|
359
|
+
open fun releaseController() {}
|
|
358
360
|
|
|
359
361
|
// Native navigation mode (glasses-rendered nav UI — G2 service 0x08 today).
|
|
360
362
|
// Default no-ops so devices without a firmware nav UI accept the calls; the JS layer
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "0.3.1",
|
|
4
|
-
"releaseIdentity": "0.3.1-dev.
|
|
5
|
-
"releaseSetId": "glassly-0.3.1-dev.
|
|
6
|
-
"sourceCommit": "
|
|
4
|
+
"releaseIdentity": "0.3.1-dev.537",
|
|
5
|
+
"releaseSetId": "glassly-0.3.1-dev.537",
|
|
6
|
+
"sourceCommit": "2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a",
|
|
7
7
|
"otaManifestUrl": null,
|
|
8
8
|
"otaManifestSha256": null
|
|
9
9
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.3.1\",\n \"releaseIdentity\": \"0.3.1-dev.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.3.1\",\n \"releaseIdentity\": \"0.3.1-dev.537\",\n \"releaseSetId\": \"glassly-0.3.1-dev.537\",\n \"sourceCommit\": \"2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
|
|
@@ -11,7 +11,7 @@ enum BluetoothSdkDefaults {
|
|
|
11
11
|
static let voiceActivityDetectionEnabled = false
|
|
12
12
|
static let loudnessGateEnabled = true
|
|
13
13
|
private static let infoSdkVersionKey = "BluetoothSdkVersion"
|
|
14
|
-
private static let swiftPackageSdkVersion = "0.3.1-dev.
|
|
14
|
+
private static let swiftPackageSdkVersion = "0.3.1-dev.537"
|
|
15
15
|
private static let swiftPackageSdkVersionPlaceholder = "__GLASSLY" + "_BLUETOOTH_SDK_VERSION__"
|
|
16
16
|
|
|
17
17
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -1976,6 +1976,12 @@ struct ViewState {
|
|
|
1976
1976
|
Bridge.log("MAN: No controller device name, returning")
|
|
1977
1977
|
return
|
|
1978
1978
|
}
|
|
1979
|
+
// The ring link outlives glasses connects/disconnects, so every
|
|
1980
|
+
// reconnect path may find it already up.
|
|
1981
|
+
if DeviceStore.shared.get("glasses", "controllerConnected") as? Bool == true {
|
|
1982
|
+
Bridge.log("MAN: Controller already connected, returning")
|
|
1983
|
+
return
|
|
1984
|
+
}
|
|
1979
1985
|
initController(defaultController)
|
|
1980
1986
|
searchingController = true
|
|
1981
1987
|
controller?.connectById(controllerDeviceName)
|
|
@@ -2074,11 +2080,8 @@ struct ViewState {
|
|
|
2074
2080
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
2075
2081
|
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
2076
2082
|
DeviceStore.shared.apply("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
2077
|
-
//
|
|
2078
|
-
|
|
2079
|
-
DeviceStore.shared.apply("glasses", "controllerConnected", false)
|
|
2080
|
-
controller?.disconnect()
|
|
2081
|
-
controller = nil // Clear the controller reference after disconnect
|
|
2083
|
+
// The ring is its own link and stays up across glasses disconnects;
|
|
2084
|
+
// disconnectController()/forgetController() are the explicit ways down.
|
|
2082
2085
|
if clearPendingConnection {
|
|
2083
2086
|
self.clearPendingConnection()
|
|
2084
2087
|
}
|
|
@@ -2098,6 +2101,11 @@ struct ViewState {
|
|
|
2098
2101
|
controller = nil // Clear the controller reference after disconnect
|
|
2099
2102
|
}
|
|
2100
2103
|
|
|
2104
|
+
/// Debug `ring_via_glasses` flipped: re-route a connected ring live.
|
|
2105
|
+
func ringRouteToggled(_ viaGlasses: Bool) {
|
|
2106
|
+
controller?.setRouteViaGlasses(viaGlasses)
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2101
2109
|
func forget() {
|
|
2102
2110
|
Bridge.log("MAN: Forgetting smart glasses")
|
|
2103
2111
|
// Call forget first to stop timers/handlers/reconnect logic
|
|
@@ -129,6 +129,8 @@ class DeviceStore {
|
|
|
129
129
|
store.set("bluetooth", "cfw_fast_ble", false)
|
|
130
130
|
// Debug: G2PERF display performance lines in the log (off = counted, not logged).
|
|
131
131
|
store.set("bluetooth", "g2_perf_logs", false)
|
|
132
|
+
// Debug: link the R1 ring to the glasses (off = ring on the phone link only).
|
|
133
|
+
store.set("bluetooth", "ring_via_glasses", false)
|
|
132
134
|
store.set("bluetooth", "display_max_fps", DisplayPacer.defaultFps)
|
|
133
135
|
}
|
|
134
136
|
|
|
@@ -247,6 +249,11 @@ class DeviceStore {
|
|
|
247
249
|
Task { await DeviceManager.shared.sgc?.cfwFastBleToggled(enabled) }
|
|
248
250
|
}
|
|
249
251
|
|
|
252
|
+
case ("bluetooth", "ring_via_glasses"):
|
|
253
|
+
if let viaGlasses = value as? Bool, (oldValue as? Bool) != viaGlasses {
|
|
254
|
+
Task { await DeviceManager.shared.ringRouteToggled(viaGlasses) }
|
|
255
|
+
}
|
|
256
|
+
|
|
250
257
|
case ("bluetooth", "display_max_fps"):
|
|
251
258
|
if let fps = value as? NSNumber {
|
|
252
259
|
DeviceManager.shared.setDisplayMaxFps(fps.intValue)
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "0.3.1"
|
|
6
|
-
static let releaseIdentity = "0.3.1-dev.
|
|
7
|
-
static let releaseSetId = "glassly-0.3.1-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
6
|
+
static let releaseIdentity = "0.3.1-dev.537"
|
|
7
|
+
static let releaseSetId = "glassly-0.3.1-dev.537"
|
|
8
|
+
static let sourceCommit = "2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a"
|
|
9
9
|
static let otaManifestUrl = ""
|
|
10
10
|
static let otaManifestSha256 = ""
|
|
11
11
|
}
|
|
@@ -58,6 +58,8 @@ protocol ControllerManager {
|
|
|
58
58
|
|
|
59
59
|
func disconnect()
|
|
60
60
|
func forget()
|
|
61
|
+
/// Route input through the glasses (debug) or the phone link (default).
|
|
62
|
+
func setRouteViaGlasses(_ viaGlasses: Bool)
|
|
61
63
|
func findCompatibleDevices()
|
|
62
64
|
func stopScan()
|
|
63
65
|
func connectById(_ id: String)
|
|
@@ -33,9 +33,46 @@ private enum R1BLE {
|
|
|
33
33
|
/// Gesture protocol marker
|
|
34
34
|
static let GESTURE_MARKER: UInt8 = 0xFF
|
|
35
35
|
|
|
36
|
+
/// `source` on emitted touch events; the same value the G2 CFW tags
|
|
37
|
+
/// ring input it relays with, so JS treats both paths alike.
|
|
38
|
+
static let RING_SOURCE: Int32 = 2
|
|
39
|
+
|
|
36
40
|
static let SCAN_TIMEOUT: TimeInterval = 15.0
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
// MARK: - Ring → phone touch report (channel 1)
|
|
44
|
+
|
|
45
|
+
/// The ring reports touch on the legacy channel-1 transport as 11 bytes
|
|
46
|
+
/// `[00][09][61][00][wireType][aux][speed][00][tick LE32]` — the same report
|
|
47
|
+
/// the Glassly CFW relays from the glasses as the `RI` SysEvent extension
|
|
48
|
+
/// (g2flash gesture_fwd.c `faceclaw_ring_report`). Wire types map onto the
|
|
49
|
+
/// gesture names the G2 path emits so miniapps see one vocabulary.
|
|
50
|
+
private enum R1TouchReport {
|
|
51
|
+
static let LENGTH = 11
|
|
52
|
+
static let OPCODE: UInt8 = 0x61
|
|
53
|
+
|
|
54
|
+
static func gestureName(wireType: UInt8) -> String? {
|
|
55
|
+
switch wireType {
|
|
56
|
+
case 0: return "tap_start"
|
|
57
|
+
case 1: return "single_tap"
|
|
58
|
+
case 2: return "double_tap"
|
|
59
|
+
case 4: return "swipe_up"
|
|
60
|
+
case 5: return "swipe_down"
|
|
61
|
+
case 8: return "tap_end"
|
|
62
|
+
case 9: return "tap_then_hold"
|
|
63
|
+
case 10: return "touch_down"
|
|
64
|
+
default: return nil
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// nil when `data` is not a touch report; `.gesture` nil for an unknown wire type.
|
|
69
|
+
static func parse(_ data: Data) -> (gesture: String?, wireType: UInt8)? {
|
|
70
|
+
let b = [UInt8](data)
|
|
71
|
+
guard b.count == LENGTH, b[0] == 0, b[1] == 9, b[2] == OPCODE, b[3] == 0 else { return nil }
|
|
72
|
+
return (gestureName(wireType: b[4]), b[4])
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
39
76
|
// MARK: - EUS transport encoder (phone → ring, channel 2)
|
|
40
77
|
|
|
41
78
|
/// Recovered firmware contract (openCFW r1/ research): every phone→ring packet
|
|
@@ -61,6 +98,12 @@ private enum R1Eus {
|
|
|
61
98
|
/// system subcommand 0x0A: 12-byte payload = two 6-byte glasses arm MACs;
|
|
62
99
|
/// the ring stores both targets and fast-advertises for them
|
|
63
100
|
static let SUBCMD_ADV_START: UInt8 = 0x0A
|
|
101
|
+
/// system subcommand 0x82 (removeRingNotify): one-byte SET erases both
|
|
102
|
+
/// stored glasses targets and the "glasses paired" flag. While the ring
|
|
103
|
+
/// holds glasses targets its touch reports go to the glasses and the
|
|
104
|
+
/// phone link carries telemetry only, so this is what puts input on the
|
|
105
|
+
/// phone link.
|
|
106
|
+
static let SUBCMD_REMOVE_RING: UInt8 = 0x82
|
|
64
107
|
|
|
65
108
|
private static func crc16Step(_ crc: UInt16, _ byte: UInt8) -> UInt16 {
|
|
66
109
|
var c = (crc >> 8) | (crc << 8)
|
|
@@ -124,7 +167,7 @@ private enum R1Eus {
|
|
|
124
167
|
// MARK: - R1 Gesture Types
|
|
125
168
|
|
|
126
169
|
private enum R1Gesture: String {
|
|
127
|
-
case hold
|
|
170
|
+
case hold = "tap_then_hold"
|
|
128
171
|
case singleTap = "single_tap"
|
|
129
172
|
case doubleTap = "double_tap"
|
|
130
173
|
case swipeUp = "swipe_up"
|
|
@@ -168,6 +211,12 @@ class R1: NSObject, ControllerManager {
|
|
|
168
211
|
private var initSequenceRun = false
|
|
169
212
|
private var eusSerial: UInt16 = 0
|
|
170
213
|
|
|
214
|
+
/// Debug `ring_via_glasses` (default off): link the ring to the glasses and
|
|
215
|
+
/// take its input through the CFW relay instead of this link.
|
|
216
|
+
private var routeViaGlasses: Bool {
|
|
217
|
+
DeviceStore.shared.get("bluetooth", "ring_via_glasses") as? Bool ?? false
|
|
218
|
+
}
|
|
219
|
+
|
|
171
220
|
/// Device search
|
|
172
221
|
var DEVICE_SEARCH_ID = "NOT_SET"
|
|
173
222
|
|
|
@@ -409,20 +458,54 @@ class R1: NSObject, ControllerManager {
|
|
|
409
458
|
|
|
410
459
|
DeviceStore.shared.apply("glasses", "controllerConnected", true)
|
|
411
460
|
// The phone link owns these flags: a ready phone link IS pairing
|
|
412
|
-
// success (fires handleControllerReady).
|
|
413
|
-
// on afterwards
|
|
461
|
+
// success (fires handleControllerReady). A glasses link is only
|
|
462
|
+
// layered on afterwards (advStart + RING_CONNECT_INFO) when debug-routed.
|
|
414
463
|
DeviceStore.shared.apply("glasses", "controllerFullyBooted", true)
|
|
415
464
|
|
|
416
|
-
|
|
417
|
-
connectToGlasses()
|
|
465
|
+
applyRoute(viaGlasses: routeViaGlasses)
|
|
418
466
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
467
|
+
startHeartbeat()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/// Phone route (default): erase the ring's stored glasses targets so its
|
|
471
|
+
/// touch reports come to this link, and have the glasses drop any ring
|
|
472
|
+
/// link they still hold. Glasses route (debug): hand the ring the glasses
|
|
473
|
+
/// MACs, then ask the glasses to connect.
|
|
474
|
+
private func applyRoute(viaGlasses: Bool) {
|
|
475
|
+
if viaGlasses {
|
|
476
|
+
connectToGlasses()
|
|
477
|
+
// after a second, connect the glasses to the controller if needed:
|
|
478
|
+
Task {
|
|
479
|
+
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
480
|
+
await DeviceManager.shared.sgc?.connectController()
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
sendEusCommand(R1Eus.SUBCMD_REMOVE_RING, payload: Data([0x01]), log: "removeRingNotify")
|
|
484
|
+
Task { await DeviceManager.shared.sgc?.releaseController() }
|
|
423
485
|
}
|
|
486
|
+
}
|
|
424
487
|
|
|
425
|
-
|
|
488
|
+
func setRouteViaGlasses(_ viaGlasses: Bool) {
|
|
489
|
+
Bridge.log("R1: route via \(viaGlasses ? "glasses" : "phone")")
|
|
490
|
+
guard DeviceStore.shared.get("glasses", "controllerConnected") as? Bool ?? false else { return }
|
|
491
|
+
applyRoute(viaGlasses: viaGlasses)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/// One emit path for both direct parsers: dropped while the ring is
|
|
495
|
+
/// debug-routed via the glasses so a lingering phone-side report can't
|
|
496
|
+
/// double up with the CFW relay.
|
|
497
|
+
private func emitGesture(_ gestureName: String, timestamp: Int64, via: String) {
|
|
498
|
+
guard !routeViaGlasses else {
|
|
499
|
+
Bridge.log("R1: \(via) \(gestureName) ignored (ring routed via glasses)")
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
Bridge.log("R1: \(via): \(gestureName)")
|
|
503
|
+
Bridge.sendTouchEvent(
|
|
504
|
+
deviceModel: ControllerTypes.R1,
|
|
505
|
+
gestureName: gestureName,
|
|
506
|
+
timestamp: timestamp,
|
|
507
|
+
source: R1BLE.RING_SOURCE
|
|
508
|
+
)
|
|
426
509
|
}
|
|
427
510
|
|
|
428
511
|
// MARK: - Heartbeat
|
|
@@ -455,15 +538,20 @@ class R1: NSObject, ControllerManager {
|
|
|
455
538
|
|
|
456
539
|
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
457
540
|
|
|
541
|
+
// Touch report: [00 09 61 00 wireType aux speed 00 tick4]
|
|
542
|
+
if let report = R1TouchReport.parse(data) {
|
|
543
|
+
if let gesture = report.gesture {
|
|
544
|
+
emitGesture(gesture, timestamp: timestamp, via: "Touch report")
|
|
545
|
+
} else {
|
|
546
|
+
Bridge.log("R1: Unknown touch report wireType=\(report.wireType)")
|
|
547
|
+
}
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
|
|
458
551
|
// Gesture: [0xFF, type, param]
|
|
459
552
|
if data.count >= 3, data[0] == R1BLE.GESTURE_MARKER {
|
|
460
553
|
if let gesture = R1Gesture.parse(from: data) {
|
|
461
|
-
|
|
462
|
-
Bridge.sendTouchEvent(
|
|
463
|
-
deviceModel: ControllerTypes.R1,
|
|
464
|
-
gestureName: gesture.rawValue,
|
|
465
|
-
timestamp: timestamp
|
|
466
|
-
)
|
|
554
|
+
emitGesture(gesture.rawValue, timestamp: timestamp, via: "Gesture")
|
|
467
555
|
} else {
|
|
468
556
|
Bridge.log(
|
|
469
557
|
"R1: Unknown gesture type=0x\(String(format: "%02X", data[1])) param=0x\(String(format: "%02X", data[2]))"
|
|
@@ -492,12 +580,7 @@ class R1: NSObject, ControllerManager {
|
|
|
492
580
|
{
|
|
493
581
|
let gestureData = Data(data[ffIndex ... ffIndex + 2])
|
|
494
582
|
if let gesture = R1Gesture.parse(from: gestureData) {
|
|
495
|
-
|
|
496
|
-
Bridge.sendTouchEvent(
|
|
497
|
-
deviceModel: ControllerTypes.R1,
|
|
498
|
-
gestureName: gesture.rawValue,
|
|
499
|
-
timestamp: timestamp
|
|
500
|
-
)
|
|
583
|
+
emitGesture(gesture.rawValue, timestamp: timestamp, via: "Embedded gesture")
|
|
501
584
|
return
|
|
502
585
|
}
|
|
503
586
|
}
|
package/ios/Source/sgcs/G2.swift
CHANGED
|
@@ -2674,7 +2674,7 @@ final class G2: NSObject, SGCManager {
|
|
|
2674
2674
|
|
|
2675
2675
|
setFullyConnected()
|
|
2676
2676
|
|
|
2677
|
-
//
|
|
2677
|
+
// Link a paired ring if it is routed via the glasses, else release it:
|
|
2678
2678
|
connectController()
|
|
2679
2679
|
|
|
2680
2680
|
// Query version + battery info from glasses
|
|
@@ -5812,66 +5812,65 @@ final class G2: NSObject, SGCManager {
|
|
|
5812
5812
|
sendEvenHubHeartbeat()
|
|
5813
5813
|
}
|
|
5814
5814
|
|
|
5815
|
-
|
|
5815
|
+
/// Debug `ring_via_glasses`: the ring is linked to the glasses and its
|
|
5816
|
+
/// input arrives through the CFW. Off (default), the ring stays on the
|
|
5817
|
+
/// phone link and the glasses must not hold it — the ring sends touch
|
|
5818
|
+
/// reports to whichever glasses link it has and only telemetry to the phone.
|
|
5819
|
+
private var ringViaGlasses: Bool {
|
|
5820
|
+
DeviceStore.shared.get("bluetooth", "ring_via_glasses") as? Bool ?? false
|
|
5821
|
+
}
|
|
5822
|
+
|
|
5823
|
+
/// RING_CONNECT_INFO for the paired ring. False when the glasses aren't
|
|
5824
|
+
/// booted or no ring MAC is known.
|
|
5825
|
+
@discardableResult
|
|
5826
|
+
private func sendRingConnectInfo(connect: Bool, tag: String) -> Bool {
|
|
5816
5827
|
let isFullyBooted = DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
|
|
5817
5828
|
guard isFullyBooted else {
|
|
5818
|
-
Bridge.log("G2:
|
|
5819
|
-
return
|
|
5829
|
+
Bridge.log("G2: \(tag) - g2 not fully booted, ignoring")
|
|
5830
|
+
return false
|
|
5820
5831
|
}
|
|
5821
5832
|
|
|
5822
|
-
guard let mac = DeviceStore.shared.get("glasses", "controllerMacAddress") as? String else {
|
|
5823
|
-
Bridge.log("G2:
|
|
5824
|
-
return
|
|
5833
|
+
guard let mac = DeviceStore.shared.get("glasses", "controllerMacAddress") as? String, !mac.isEmpty else {
|
|
5834
|
+
Bridge.log("G2: \(tag) - no MAC address found")
|
|
5835
|
+
return false
|
|
5825
5836
|
}
|
|
5826
5837
|
|
|
5827
5838
|
// Parse "AA:BB:CC:DD:EE:FF" into 6-byte Data
|
|
5828
5839
|
let hexParts = mac.split(separator: ":").compactMap { UInt8($0, radix: 16) }
|
|
5829
5840
|
guard hexParts.count == 6 else {
|
|
5830
|
-
Bridge.log("G2:
|
|
5831
|
-
return
|
|
5841
|
+
Bridge.log("G2: \(tag) - invalid MAC format: \(mac)")
|
|
5842
|
+
return false
|
|
5832
5843
|
}
|
|
5833
|
-
let macData = Data(hexParts)
|
|
5834
5844
|
|
|
5835
5845
|
let msg = DevSettingsProto.ringConnectInfo(
|
|
5836
5846
|
magicRandom: sendManager.nextMagicRandom(),
|
|
5837
|
-
connect:
|
|
5838
|
-
ringMac:
|
|
5847
|
+
connect: connect,
|
|
5848
|
+
ringMac: Data(hexParts)
|
|
5839
5849
|
)
|
|
5840
5850
|
sendDevSettingsCommand(msg)
|
|
5841
|
-
Bridge.log("G2: Sent
|
|
5851
|
+
Bridge.log("G2: Sent RING_\(connect ? "CONNECT" : "DISCONNECT")_INFO for MAC \(mac)")
|
|
5852
|
+
return true
|
|
5842
5853
|
}
|
|
5843
5854
|
|
|
5844
|
-
func
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
return
|
|
5849
|
-
}
|
|
5850
|
-
|
|
5851
|
-
guard let mac = DeviceStore.shared.get("glasses", "controllerMacAddress") as? String else {
|
|
5852
|
-
Bridge.log("G2: disconnectController - no MAC address found")
|
|
5853
|
-
return
|
|
5854
|
-
}
|
|
5855
|
-
|
|
5856
|
-
// Parse "AA:BB:CC:DD:EE:FF" into 6-byte Data
|
|
5857
|
-
let hexParts = mac.split(separator: ":").compactMap { UInt8($0, radix: 16) }
|
|
5858
|
-
guard hexParts.count == 6 else {
|
|
5859
|
-
Bridge.log("G2: disconnectController - invalid MAC format: \(mac)")
|
|
5855
|
+
func connectController() {
|
|
5856
|
+
guard ringViaGlasses else {
|
|
5857
|
+
// Phone-routed ring: make sure the glasses let go of it instead.
|
|
5858
|
+
releaseController()
|
|
5860
5859
|
return
|
|
5861
5860
|
}
|
|
5862
|
-
|
|
5861
|
+
sendRingConnectInfo(connect: true, tag: "connectController")
|
|
5862
|
+
}
|
|
5863
5863
|
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
connect: false,
|
|
5867
|
-
ringMac: macData
|
|
5868
|
-
)
|
|
5869
|
-
sendDevSettingsCommand(msg)
|
|
5864
|
+
func disconnectController() {
|
|
5865
|
+
guard sendRingConnectInfo(connect: false, tag: "disconnectController") else { return }
|
|
5870
5866
|
|
|
5871
5867
|
// DeviceStore.shared.apply("glasses", "controllerMacAddress", "")
|
|
5872
5868
|
DeviceStore.shared.apply("glasses", "controllerConnected", false)
|
|
5873
5869
|
DeviceStore.shared.apply("glasses", "controllerFullyBooted", false)
|
|
5874
|
-
|
|
5870
|
+
}
|
|
5871
|
+
|
|
5872
|
+
func releaseController() {
|
|
5873
|
+
sendRingConnectInfo(connect: false, tag: "releaseController")
|
|
5875
5874
|
}
|
|
5876
5875
|
|
|
5877
5876
|
/// Fire an EvenAI skill — the same path "Hey Even, show X" uses. Triggers a
|
|
@@ -7092,13 +7091,24 @@ final class G2: NSObject, SGCManager {
|
|
|
7092
7091
|
return
|
|
7093
7092
|
}
|
|
7094
7093
|
|
|
7094
|
+
// Ring input relayed by the glasses. Older CFWs left event 14's
|
|
7095
|
+
// source unspecified, so the id alone also marks the ring.
|
|
7096
|
+
let fromRing = eventSource == 2 || eventType == .ringTouchDown
|
|
7097
|
+
if fromRing, !ringViaGlasses {
|
|
7098
|
+
// The phone link owns ring input; the glasses shouldn't hold
|
|
7099
|
+
// the ring at all, so this is a stale link — drop it again.
|
|
7100
|
+
Bridge.log("G2: ring \(gestureName) via glasses ignored (ring routed via phone)")
|
|
7101
|
+
releaseController()
|
|
7102
|
+
return
|
|
7103
|
+
}
|
|
7104
|
+
|
|
7095
7105
|
Bridge.sendTouchEvent(
|
|
7096
7106
|
deviceModel: type, gestureName: gestureName,
|
|
7097
7107
|
timestamp: timestamp,
|
|
7098
7108
|
source: eventSource
|
|
7099
7109
|
)
|
|
7100
7110
|
|
|
7101
|
-
if
|
|
7111
|
+
if fromRing {
|
|
7102
7112
|
// controller must be connected and fully booted:
|
|
7103
7113
|
setControllerFullyConnected()
|
|
7104
7114
|
}
|
|
@@ -7259,7 +7269,8 @@ final class G2: NSObject, SGCManager {
|
|
|
7259
7269
|
if connStatus == 22 {
|
|
7260
7270
|
// The ring↔glasses link dropped. The phone↔ring link owns
|
|
7261
7271
|
// controllerConnected/FullyBooted, so don't clear them here
|
|
7262
|
-
// (that would flap pairing state) — just re-request the link
|
|
7272
|
+
// (that would flap pairing state) — just re-request the link
|
|
7273
|
+
// (a no-op while the ring is routed via the phone).
|
|
7263
7274
|
Bridge.log("G2: Ring-glasses link dropped, re-requesting")
|
|
7264
7275
|
reconnectController()
|
|
7265
7276
|
}
|
|
@@ -343,6 +343,8 @@ protocol SGCManager {
|
|
|
343
343
|
func getConnectedBluetoothName() -> String?
|
|
344
344
|
func connectController()
|
|
345
345
|
func disconnectController()
|
|
346
|
+
/// Drop the glasses↔ring link without touching the phone↔ring state.
|
|
347
|
+
func releaseController()
|
|
346
348
|
func cleanup()
|
|
347
349
|
func ping()
|
|
348
350
|
func dbg1()
|
|
@@ -397,6 +399,9 @@ extension SGCManager {
|
|
|
397
399
|
/// Default: no-op. Only G2 distinguishes stock from Glassly CFW firmware.
|
|
398
400
|
func awaitFirmwareClassification() async {}
|
|
399
401
|
|
|
402
|
+
/// Default: no-op. Only G2 can hold a ring link.
|
|
403
|
+
func releaseController() {}
|
|
404
|
+
|
|
400
405
|
/// Default: no-op debug hooks; only G2 wires them.
|
|
401
406
|
func dbg3() {}
|
|
402
407
|
func dbg4() {}
|
package/package.json
CHANGED
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.3.1",
|
|
15
|
-
"releaseIdentity": "0.3.1-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.3.1-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.1-dev.537",
|
|
16
|
+
"releaseSetId": "glassly-0.3.1-dev.537",
|
|
17
|
+
"sourceCommit": "2cb6ba96b1141a7d23dc38cb1ce44e6ede4f305a",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|