@mentra/bluetooth-sdk 3.3.0-dev.516 → 3.3.0-dev.523
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/mentra/bluetoothsdk/BluetoothSdkModule.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +55 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +63 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +26 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +31 -19
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +111 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +44 -4
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +82 -13
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +94 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +15 -9
- package/build/BluetoothSdk.types.d.ts +2 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +2 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +4 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +66 -50
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G2.swift +259 -245
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Nimo.swift +104 -43
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +20 -6
- package/ios/Source/sgcs/NimoCanvasSession.swift +30 -14
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +74 -5
- package/ios/Tests/NimoCanvasTransportTests.swift +25 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +3 -0
- package/src/_private/BluetoothSdkModule.ts +2 -0
- package/src/generated/releaseMetadata.ts +5 -5
package/ios/Source/sgcs/G2.swift
CHANGED
|
@@ -1386,8 +1386,9 @@ private final class ImgAckBox {
|
|
|
1386
1386
|
|
|
1387
1387
|
// MARK: - G2 Class (SGCManager implementation)
|
|
1388
1388
|
|
|
1389
|
-
///
|
|
1390
|
-
|
|
1389
|
+
/// Reconnection attempts share the driver's main-actor lifecycle.
|
|
1390
|
+
@MainActor
|
|
1391
|
+
final class G2ReconnectionManager {
|
|
1391
1392
|
private var task: Task<Void, Never>?
|
|
1392
1393
|
private let intervalSeconds: TimeInterval
|
|
1393
1394
|
private var attempts = 0
|
|
@@ -1459,10 +1460,10 @@ class G2: NSObject, SGCManager {
|
|
|
1459
1460
|
private var rightNotifyChar: CBCharacteristic?
|
|
1460
1461
|
private var rightAudioChar: CBCharacteristic?
|
|
1461
1462
|
private var leftAudioChar: CBCharacteristic?
|
|
1462
|
-
private var
|
|
1463
|
-
private var
|
|
1464
|
-
private var
|
|
1465
|
-
private var
|
|
1463
|
+
private var connectionSession = G2ConnectionSession()
|
|
1464
|
+
private var partialConnection = G2PartialConnection()
|
|
1465
|
+
private var partialConnectionTimer: DispatchWorkItem?
|
|
1466
|
+
private var authTask: Task<Void, Never>?
|
|
1466
1467
|
private var isDisconnecting = false
|
|
1467
1468
|
private var pairingTimeoutTimer: DispatchWorkItem?
|
|
1468
1469
|
private var useEvenDashboard = true
|
|
@@ -1528,7 +1529,7 @@ class G2: NSObject, SGCManager {
|
|
|
1528
1529
|
|
|
1529
1530
|
// Protocol state
|
|
1530
1531
|
private let sendManager = G2SendManager()
|
|
1531
|
-
private
|
|
1532
|
+
private var receiveManager = G2ReceiveManager()
|
|
1532
1533
|
private var foregroundObserver: NSObjectProtocol?
|
|
1533
1534
|
private var startupPageCreated: Bool = false // createStartUpPageContainer can only be called once
|
|
1534
1535
|
private var pageCreated: Bool = false
|
|
@@ -1564,7 +1565,6 @@ class G2: NSObject, SGCManager {
|
|
|
1564
1565
|
/// How many redundant resends each text update gets (text has no ACK). The reconcile loop sets a
|
|
1565
1566
|
/// container's `pendingSends` to `1 + EVEN_HUB_RESEND_COUNT` on change.
|
|
1566
1567
|
private let EVEN_HUB_RESEND_COUNT: Int = 1
|
|
1567
|
-
private var authStarted: Bool = false
|
|
1568
1568
|
|
|
1569
1569
|
/// Dashboard menu: appId → packageName mapping for selection reverse lookup
|
|
1570
1570
|
private var menuAppIdToPackageName: [Int32: String] = [:]
|
|
@@ -1754,16 +1754,17 @@ class G2: NSObject, SGCManager {
|
|
|
1754
1754
|
if leftDraining { return }
|
|
1755
1755
|
leftDraining = true
|
|
1756
1756
|
}
|
|
1757
|
+
let generation = connectionSession.generation
|
|
1757
1758
|
Task { @MainActor [weak self] in
|
|
1758
|
-
await self?.drainLoop(right: right)
|
|
1759
|
+
await self?.drainLoop(right: right, generation: generation)
|
|
1759
1760
|
}
|
|
1760
1761
|
}
|
|
1761
1762
|
|
|
1762
1763
|
/// Drain one side's queue: write each packet directly (`.withoutResponse`), paced by a small
|
|
1763
1764
|
/// sleep. No `canSend` gate (see note above). Runs until the queue is empty, then clears the
|
|
1764
1765
|
/// per-side flag so the next enqueue restarts it.
|
|
1765
|
-
private func drainLoop(right: Bool) async {
|
|
1766
|
-
while
|
|
1766
|
+
private func drainLoop(right: Bool, generation: UInt64) async {
|
|
1767
|
+
while connectionSession.generation == generation && !Task.isCancelled {
|
|
1767
1768
|
guard let peripheral = right ? rightPeripheral : leftPeripheral,
|
|
1768
1769
|
let char = right ? rightWriteChar : leftWriteChar
|
|
1769
1770
|
else {
|
|
@@ -1891,20 +1892,19 @@ class G2: NSObject, SGCManager {
|
|
|
1891
1892
|
|
|
1892
1893
|
// MARK: - Authentication Sequence
|
|
1893
1894
|
|
|
1894
|
-
private func
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
sendDevSettingsCommand(authL, left: true, right: false)
|
|
1899
|
-
}
|
|
1895
|
+
private func startAuthenticationIfReady() {
|
|
1896
|
+
guard let generation = connectionSession.beginAuthentication() else { return }
|
|
1897
|
+
Bridge.log("G2: Both sides initialized, starting auth sequence")
|
|
1898
|
+
authTask = Task { [weak self] in await self?.runAuthSequence(generation: generation) }
|
|
1900
1899
|
}
|
|
1901
1900
|
|
|
1902
|
-
private func
|
|
1903
|
-
|
|
1904
|
-
|
|
1901
|
+
private func waitForAuthStep(generation: UInt64) async -> Bool {
|
|
1902
|
+
do { try await Task.sleep(nanoseconds: 200_000_000) } catch { return false }
|
|
1903
|
+
return !Task.isCancelled && connectionSession.generation == generation && !isDisconnecting
|
|
1905
1904
|
}
|
|
1906
1905
|
|
|
1907
|
-
private func runAuthSequence() async {
|
|
1906
|
+
private func runAuthSequence(generation: UInt64) async {
|
|
1907
|
+
guard !Task.isCancelled, connectionSession.generation == generation, !isDisconnecting else { return }
|
|
1908
1908
|
Bridge.log("G2: Running auth sequence")
|
|
1909
1909
|
|
|
1910
1910
|
// Auth to left side
|
|
@@ -1914,19 +1914,19 @@ class G2: NSObject, SGCManager {
|
|
|
1914
1914
|
}
|
|
1915
1915
|
|
|
1916
1916
|
// Small delay then auth right + pipe role change + time sync
|
|
1917
|
-
|
|
1917
|
+
guard await waitForAuthStep(generation: generation) else { return }
|
|
1918
1918
|
|
|
1919
1919
|
let authR = DevSettingsProto.authCmd(magicRandom: sendManager.nextMagicRandom())
|
|
1920
1920
|
sendDevSettingsCommand(authR, left: false, right: true)
|
|
1921
1921
|
|
|
1922
|
-
|
|
1922
|
+
guard await waitForAuthStep(generation: generation) else { return }
|
|
1923
1923
|
|
|
1924
1924
|
let roleChange = DevSettingsProto.pipeRoleChange(
|
|
1925
1925
|
magicRandom: sendManager.nextMagicRandom()
|
|
1926
1926
|
)
|
|
1927
1927
|
sendDevSettingsCommand(roleChange, left: false, right: true)
|
|
1928
1928
|
|
|
1929
|
-
|
|
1929
|
+
guard await waitForAuthStep(generation: generation) else { return }
|
|
1930
1930
|
|
|
1931
1931
|
let timeSync = DevSettingsProto.timeSync(
|
|
1932
1932
|
magicRandom: sendManager.nextMagicRandom()
|
|
@@ -1934,7 +1934,7 @@ class G2: NSObject, SGCManager {
|
|
|
1934
1934
|
sendDevSettingsCommand(timeSync, left: true, right: true)
|
|
1935
1935
|
|
|
1936
1936
|
// Skip onboarding on connect
|
|
1937
|
-
|
|
1937
|
+
guard await waitForAuthStep(generation: generation) else { return }
|
|
1938
1938
|
let onboarding = OnboardingProto.skipOnboarding(
|
|
1939
1939
|
magicRandom: sendManager.nextMagicRandom()
|
|
1940
1940
|
)
|
|
@@ -2018,11 +2018,21 @@ class G2: NSObject, SGCManager {
|
|
|
2018
2018
|
|
|
2019
2019
|
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
2020
2020
|
|
|
2021
|
-
|
|
2022
|
-
|
|
2021
|
+
connectionSession.completeSetup(generation: generation)
|
|
2022
|
+
finishConnectionIfReady()
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
private func finishConnectionIfReady() {
|
|
2026
|
+
guard connectionSession.isReady, !isDisconnecting,
|
|
2027
|
+
leftPeripheral?.state == .connected, rightPeripheral?.state == .connected,
|
|
2028
|
+
DeviceStore.shared.get("glasses", "fullyBooted") as? Bool != true
|
|
2029
|
+
else { return }
|
|
2023
2030
|
|
|
2024
|
-
|
|
2025
|
-
|
|
2031
|
+
cancelPairingTimeout()
|
|
2032
|
+
updatePartialConnection()
|
|
2033
|
+
startHeartbeats()
|
|
2034
|
+
reconnectionManager.stop()
|
|
2035
|
+
Bridge.log("G2: Both arms authenticated and initialization complete, glasses ready")
|
|
2026
2036
|
|
|
2027
2037
|
// Set device_name so DeviceManager can save it for reconnection
|
|
2028
2038
|
let peripheralName = rightPeripheral?.name ?? leftPeripheral?.name
|
|
@@ -2048,6 +2058,8 @@ class G2: NSObject, SGCManager {
|
|
|
2048
2058
|
DeviceStore.shared.apply("glasses", "deviceModel", DeviceTypes.G2)
|
|
2049
2059
|
|
|
2050
2060
|
setFullyConnected()
|
|
2061
|
+
sendEvenHubHeartbeat()
|
|
2062
|
+
sendDevSettingsHeartbeat()
|
|
2051
2063
|
|
|
2052
2064
|
// connnect a controller if we have one:
|
|
2053
2065
|
connectController()
|
|
@@ -2973,11 +2985,14 @@ class G2: NSObject, SGCManager {
|
|
|
2973
2985
|
|
|
2974
2986
|
/// re-creates the containers and re-sends all images to the glasses:
|
|
2975
2987
|
private func rebuildState() async {
|
|
2988
|
+
guard connectionSession.isReady, !isDisconnecting else { return }
|
|
2989
|
+
let generation = connectionSession.generation
|
|
2976
2990
|
Bridge.log("G2: rebuildState()")
|
|
2977
2991
|
// recreate the containers (sets pageCreated = true; embeds text content directly):
|
|
2978
2992
|
createPageWithContainers()
|
|
2979
2993
|
|
|
2980
|
-
try
|
|
2994
|
+
do { try await Task.sleep(nanoseconds: 300_000_000) } catch { return }
|
|
2995
|
+
guard connectionSession.generation == generation, connectionSession.isReady else { return }
|
|
2981
2996
|
// Mark every image container dirty and let the reconcile loop re-send them, one at a time.
|
|
2982
2997
|
// Doing the sends here directly is what used to race a concurrent displayBitmap and clobber
|
|
2983
2998
|
// imgAckBox; routing through the dirty flag keeps a single sender (see displayReconcileTask).
|
|
@@ -2990,7 +3005,8 @@ class G2: NSObject, SGCManager {
|
|
|
2990
3005
|
}
|
|
2991
3006
|
signalDisplayDirty()
|
|
2992
3007
|
|
|
2993
|
-
try
|
|
3008
|
+
do { try await Task.sleep(nanoseconds: 300_000_000) } catch { return }
|
|
3009
|
+
guard connectionSession.generation == generation, connectionSession.isReady else { return }
|
|
2994
3010
|
syncImuReportingWithIntent()
|
|
2995
3011
|
confirmImuReporting(
|
|
2996
3012
|
forPageGeneration: pageGeneration,
|
|
@@ -3005,6 +3021,7 @@ class G2: NSObject, SGCManager {
|
|
|
3005
3021
|
/// guard each one triggered a rebuild that was torn down again → rebuild→exit→rebuild
|
|
3006
3022
|
/// storm. At most one rebuild in flight, and at most one per RECOVERY_DEBOUNCE_MS.
|
|
3007
3023
|
private func recoverPageAndMic(reason: String) {
|
|
3024
|
+
guard connectionSession.isReady, !isDisconnecting else { return }
|
|
3008
3025
|
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
|
3009
3026
|
// If the page is already alive and the mic matches intent, there's nothing to recover —
|
|
3010
3027
|
// this is a spurious/phantom firmware event (it spams close/exit even when our page is
|
|
@@ -3023,11 +3040,13 @@ class G2: NSObject, SGCManager {
|
|
|
3023
3040
|
return
|
|
3024
3041
|
}
|
|
3025
3042
|
recoveryInFlight = true
|
|
3043
|
+
let generation = connectionSession.generation
|
|
3026
3044
|
lastRecoveryRebuildMs = now
|
|
3027
3045
|
Bridge.log("G2: recover(\(reason)) — rebuilding EvenHub page")
|
|
3028
3046
|
Task { [weak self] in
|
|
3029
3047
|
guard let self = self else { return }
|
|
3030
3048
|
await self.rebuildState()
|
|
3049
|
+
guard self.connectionSession.generation == generation, self.connectionSession.isReady else { return }
|
|
3031
3050
|
// Reconcile against DeviceManager's authoritative current view so the glasses
|
|
3032
3051
|
// match the phone, not just the last-cached G2 containers.
|
|
3033
3052
|
DeviceManager.shared.sendCurrentState()
|
|
@@ -3649,28 +3668,67 @@ class G2: NSObject, SGCManager {
|
|
|
3649
3668
|
|
|
3650
3669
|
func findCompatibleDevices() {
|
|
3651
3670
|
Bridge.log("G2: findCompatibleDevices()")
|
|
3671
|
+
disconnect()
|
|
3672
|
+
isDisconnecting = false
|
|
3652
3673
|
DEVICE_SEARCH_ID = "NOT_SET"
|
|
3653
3674
|
startScan()
|
|
3654
3675
|
}
|
|
3655
3676
|
|
|
3656
3677
|
func connectById(_ id: String) {
|
|
3657
3678
|
Bridge.log("G2: connectById(\(id))")
|
|
3679
|
+
disconnect()
|
|
3658
3680
|
DEVICE_SEARCH_ID = id
|
|
3681
|
+
isDisconnecting = false
|
|
3659
3682
|
startScan()
|
|
3660
|
-
startPairingTimeout()
|
|
3661
3683
|
}
|
|
3662
3684
|
|
|
3663
3685
|
private func startPairingTimeout() {
|
|
3664
|
-
pairingTimeoutTimer
|
|
3686
|
+
guard pairingTimeoutTimer == nil, !connectionSession.isReady,
|
|
3687
|
+
DEVICE_SEARCH_ID != "NOT_SET", !DEVICE_SEARCH_ID.isEmpty else { return }
|
|
3688
|
+
let generation = connectionSession.generation
|
|
3665
3689
|
let work = DispatchWorkItem { [weak self] in
|
|
3666
|
-
guard let self
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3690
|
+
guard let self, !self.isDisconnecting,
|
|
3691
|
+
self.connectionSession.needsRecovery(attempt: generation) else { return }
|
|
3692
|
+
Bridge.log("G2: Pair initialization timed out; retrying both arms")
|
|
3693
|
+
self.updatePartialConnection()
|
|
3694
|
+
let error = self.partialConnection.timeoutError
|
|
3695
|
+
self.recoverConnection()
|
|
3696
|
+
Bridge.sendPairFailureEvent(error)
|
|
3671
3697
|
}
|
|
3672
3698
|
pairingTimeoutTimer = work
|
|
3673
|
-
DispatchQueue.main.asyncAfter(deadline: .now() +
|
|
3699
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: work)
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
/// Publish a durable status field so Home and pairing agree, including after a remount.
|
|
3703
|
+
/// Do not flash a warning for the normal interval between the two BLE connections.
|
|
3704
|
+
private func updatePartialConnection() {
|
|
3705
|
+
partialConnectionTimer?.cancel()
|
|
3706
|
+
partialConnectionTimer = nil
|
|
3707
|
+
partialConnection.update(
|
|
3708
|
+
leftConnected: leftPeripheral?.state == .connected,
|
|
3709
|
+
rightConnected: rightPeripheral?.state == .connected,
|
|
3710
|
+
now: ProcessInfo.processInfo.systemUptime
|
|
3711
|
+
)
|
|
3712
|
+
publishPartialConnection()
|
|
3713
|
+
guard partialConnection.missingSide != nil else { return }
|
|
3714
|
+
let generation = connectionSession.generation
|
|
3715
|
+
let work = DispatchWorkItem { [weak self] in
|
|
3716
|
+
guard let self, !self.isDisconnecting, self.connectionSession.generation == generation else { return }
|
|
3717
|
+
self.partialConnection.update(
|
|
3718
|
+
leftConnected: self.leftPeripheral?.state == .connected,
|
|
3719
|
+
rightConnected: self.rightPeripheral?.state == .connected,
|
|
3720
|
+
now: ProcessInfo.processInfo.systemUptime
|
|
3721
|
+
)
|
|
3722
|
+
self.publishPartialConnection()
|
|
3723
|
+
}
|
|
3724
|
+
partialConnectionTimer = work
|
|
3725
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + G2PartialConnection.noticeDelay, execute: work)
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
private func publishPartialConnection() {
|
|
3729
|
+
let side = partialConnection.visibleMissingSide(now: ProcessInfo.processInfo.systemUptime)
|
|
3730
|
+
let value: Any = side.map { $0 == .left ? "left" : "right" } as Any? ?? NSNull()
|
|
3731
|
+
DeviceStore.shared.apply("glasses", "g2MissingArm", value)
|
|
3674
3732
|
}
|
|
3675
3733
|
|
|
3676
3734
|
private func cancelPairingTimeout() {
|
|
@@ -3678,49 +3736,67 @@ class G2: NSObject, SGCManager {
|
|
|
3678
3736
|
pairingTimeoutTimer = nil
|
|
3679
3737
|
}
|
|
3680
3738
|
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3739
|
+
/// Retire the entire attempt before cancelling BLE, so late callbacks and suspended work
|
|
3740
|
+
/// cannot revive it. Only release peripherals belonging to this selected pair.
|
|
3741
|
+
private func resetConnection() {
|
|
3742
|
+
connectionSession.reset()
|
|
3743
|
+
partialConnectionTimer?.cancel()
|
|
3744
|
+
partialConnectionTimer = nil
|
|
3745
|
+
partialConnection = G2PartialConnection()
|
|
3746
|
+
publishPartialConnection()
|
|
3747
|
+
authTask?.cancel()
|
|
3748
|
+
authTask = nil
|
|
3687
3749
|
cancelPairingTimeout()
|
|
3688
3750
|
stopHeartbeats()
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
for peripheral in connected {
|
|
3702
|
-
centralManager?.cancelPeripheralConnection(peripheral)
|
|
3703
|
-
}
|
|
3704
|
-
|
|
3751
|
+
centralManager?.stopScan()
|
|
3752
|
+
notificationEpoch += 1
|
|
3753
|
+
notificationControlMagic = nil
|
|
3754
|
+
let peripherals = [leftPeripheral, rightPeripheral].compactMap { $0 }
|
|
3755
|
+
leftPeripheral = nil
|
|
3756
|
+
rightPeripheral = nil
|
|
3757
|
+
leftWriteChar = nil
|
|
3758
|
+
rightWriteChar = nil
|
|
3759
|
+
leftNotifyChar = nil
|
|
3760
|
+
rightNotifyChar = nil
|
|
3761
|
+
leftAudioChar = nil
|
|
3762
|
+
rightAudioChar = nil
|
|
3705
3763
|
leftWriteQueue.removeAll()
|
|
3706
3764
|
rightWriteQueue.removeAll()
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
leftAuthenticated = false
|
|
3711
|
-
rightAuthenticated = false
|
|
3765
|
+
leftDraining = false
|
|
3766
|
+
rightDraining = false
|
|
3767
|
+
receiveManager = G2ReceiveManager()
|
|
3712
3768
|
startupPageCreated = false
|
|
3713
3769
|
pageCreated = false
|
|
3770
|
+
pageGeneration &+= 1
|
|
3771
|
+
evenHubMicActive = false
|
|
3772
|
+
recoveryInFlight = false
|
|
3773
|
+
lastRecoveryRebuildMs = 0
|
|
3714
3774
|
dashboardShowing = 0
|
|
3715
3775
|
dashboardOpening = false
|
|
3716
3776
|
heartbeatCounter = 0
|
|
3717
3777
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
3718
3778
|
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
3779
|
+
for peripheral in peripherals {
|
|
3780
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
3781
|
+
}
|
|
3782
|
+
publishNotificationStatus()
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
private func recoverConnection() {
|
|
3786
|
+
resetConnection()
|
|
3787
|
+
startReconnectionTimer()
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
func disconnect() {
|
|
3791
|
+
Bridge.log("G2: disconnect()")
|
|
3792
|
+
isDisconnecting = true
|
|
3793
|
+
resetConnection()
|
|
3794
|
+
reconnectionManager.stop()
|
|
3719
3795
|
}
|
|
3720
3796
|
|
|
3721
3797
|
func forget() {
|
|
3722
3798
|
stopHeartbeats()
|
|
3723
|
-
|
|
3799
|
+
reconnectionManager.stop()
|
|
3724
3800
|
disconnect()
|
|
3725
3801
|
// Note: leftGlassUUIDMap / rightGlassUUIDMap intentionally preserved so a future
|
|
3726
3802
|
// pair to the same serial number can reuse the cached peripheral UUID.
|
|
@@ -3960,7 +4036,7 @@ class G2: NSObject, SGCManager {
|
|
|
3960
4036
|
// } else {
|
|
3961
4037
|
// stopCompass()
|
|
3962
4038
|
// }
|
|
3963
|
-
|
|
4039
|
+
startAuthenticationIfReady()
|
|
3964
4040
|
}
|
|
3965
4041
|
|
|
3966
4042
|
/// Start a navigation session so the glasses stream compass heading via
|
|
@@ -4182,6 +4258,7 @@ class G2: NSObject, SGCManager {
|
|
|
4182
4258
|
|
|
4183
4259
|
@discardableResult
|
|
4184
4260
|
private func startScan() -> Bool {
|
|
4261
|
+
guard !isDisconnecting else { return false }
|
|
4185
4262
|
Bridge.log("G2: startScan()")
|
|
4186
4263
|
if centralManager == nil {
|
|
4187
4264
|
centralManager = CBCentralManager(
|
|
@@ -4190,44 +4267,16 @@ class G2: NSObject, SGCManager {
|
|
|
4190
4267
|
)
|
|
4191
4268
|
}
|
|
4192
4269
|
|
|
4193
|
-
isDisconnecting = false
|
|
4194
4270
|
guard centralManager!.state == .poweredOn else {
|
|
4195
4271
|
Bridge.log("G2: Bluetooth not powered on")
|
|
4196
4272
|
return false
|
|
4197
4273
|
}
|
|
4198
4274
|
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
if name.contains("_L_") && serialNumber.contains(DEVICE_SEARCH_ID) {
|
|
4206
|
-
leftPeripheral = device
|
|
4207
|
-
device.delegate = self
|
|
4208
|
-
device.discoverServices([G2BLE.SERVICE_UUID])
|
|
4209
|
-
centralManager!.connect(
|
|
4210
|
-
leftPeripheral!,
|
|
4211
|
-
options: notificationConnectionOptions
|
|
4212
|
-
)
|
|
4213
|
-
} else if name.contains("_R_") && serialNumber.contains(DEVICE_SEARCH_ID) {
|
|
4214
|
-
rightPeripheral = device
|
|
4215
|
-
device.delegate = self
|
|
4216
|
-
device.discoverServices([G2BLE.SERVICE_UUID])
|
|
4217
|
-
centralManager!.connect(
|
|
4218
|
-
rightPeripheral!,
|
|
4219
|
-
options: notificationConnectionOptions
|
|
4220
|
-
)
|
|
4221
|
-
}
|
|
4222
|
-
// we can't emit the serial number here unfortunately:
|
|
4223
|
-
emitDiscoveredDevice(serialNumber)
|
|
4224
|
-
}
|
|
4225
|
-
}
|
|
4226
|
-
|
|
4227
|
-
// Try UUID-based reconnection first
|
|
4228
|
-
if connectByUUID() {
|
|
4229
|
-
return true
|
|
4230
|
-
}
|
|
4275
|
+
startPairingTimeout()
|
|
4276
|
+
// UUIDs are scoped to the selected serial. Reclaim each saved arm independently,
|
|
4277
|
+
// including one still connected to iOS after a process restart, then scan for its peer.
|
|
4278
|
+
connectKnownPeripherals()
|
|
4279
|
+
if leftPeripheral != nil && rightPeripheral != nil { return true }
|
|
4231
4280
|
|
|
4232
4281
|
centralManager!.scanForPeripherals(
|
|
4233
4282
|
withServices: nil,
|
|
@@ -4242,47 +4291,34 @@ class G2: NSObject, SGCManager {
|
|
|
4242
4291
|
centralManager?.stopScan()
|
|
4243
4292
|
}
|
|
4244
4293
|
|
|
4245
|
-
private func
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4294
|
+
private func connectKnownPeripherals() {
|
|
4295
|
+
guard DEVICE_SEARCH_ID != "NOT_SET", !DEVICE_SEARCH_ID.isEmpty else { return }
|
|
4296
|
+
let leftUUID = leftGlassUUID(forSN: DEVICE_SEARCH_ID)
|
|
4297
|
+
let rightUUID = rightGlassUUID(forSN: DEVICE_SEARCH_ID)
|
|
4298
|
+
let cachedIDs = [leftUUID, rightUUID].compactMap { $0 }
|
|
4299
|
+
let cached = centralManager?.retrievePeripherals(withIdentifiers: cachedIDs) ?? []
|
|
4300
|
+
for peripheral in cached + getConnectedDevices() {
|
|
4301
|
+
let serial = peripheral.name.flatMap { deviceNameToSerialNumber[$0] }
|
|
4302
|
+
guard let side = G2ConnectionTarget.side(
|
|
4303
|
+
identifier: peripheral.identifier, name: peripheral.name,
|
|
4304
|
+
advertisedSerial: serial, searchID: DEVICE_SEARCH_ID,
|
|
4305
|
+
leftUUID: leftUUID, rightUUID: rightUUID
|
|
4306
|
+
) else { continue }
|
|
4307
|
+
connectPeripheral(peripheral, side: side)
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
private func connectPeripheral(_ peripheral: CBPeripheral, side: G2ConnectionSession.Side) {
|
|
4312
|
+
guard !isDisconnecting, peripheral.state != .disconnecting else { return }
|
|
4313
|
+
if side == .left {
|
|
4314
|
+
guard leftPeripheral == nil else { return }
|
|
4315
|
+
leftPeripheral = peripheral
|
|
4316
|
+
} else {
|
|
4317
|
+
guard rightPeripheral == nil else { return }
|
|
4318
|
+
rightPeripheral = peripheral
|
|
4250
4319
|
}
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
let rightUUID = rightGlassUUID(forSN: DEVICE_SEARCH_ID)
|
|
4254
|
-
else { return false }
|
|
4255
|
-
|
|
4256
|
-
let knownLeft = centralManager?.retrievePeripherals(withIdentifiers: [leftUUID])
|
|
4257
|
-
let knownRight = centralManager?.retrievePeripherals(withIdentifiers: [rightUUID])
|
|
4258
|
-
|
|
4259
|
-
guard let left = knownLeft?.first, let right = knownRight?.first else { return false }
|
|
4260
|
-
|
|
4261
|
-
// Validate the cached peripherals match the device the user selected
|
|
4262
|
-
let leftName = left.name ?? ""
|
|
4263
|
-
let rightName = right.name ?? ""
|
|
4264
|
-
// if !leftName.isEmpty && !leftName.contains(DEVICE_SEARCH_ID) {
|
|
4265
|
-
// Bridge.log(
|
|
4266
|
-
// "G2: connectByUUID - cached left '\(leftName)' doesn't match search ID '\(DEVICE_SEARCH_ID)', skipping"
|
|
4267
|
-
// )
|
|
4268
|
-
// return false
|
|
4269
|
-
// }
|
|
4270
|
-
// if !rightName.isEmpty && !rightName.contains(DEVICE_SEARCH_ID) {
|
|
4271
|
-
// Bridge.log(
|
|
4272
|
-
// "G2: connectByUUID - cached right '\(rightName)' doesn't match search ID '\(DEVICE_SEARCH_ID)', skipping"
|
|
4273
|
-
// )
|
|
4274
|
-
// return false
|
|
4275
|
-
// }
|
|
4276
|
-
|
|
4277
|
-
Bridge.log("G2: connectByUUID - left: \(leftName), right: \(rightName)")
|
|
4278
|
-
|
|
4279
|
-
leftPeripheral = left
|
|
4280
|
-
rightPeripheral = right
|
|
4281
|
-
left.delegate = self
|
|
4282
|
-
right.delegate = self
|
|
4283
|
-
centralManager?.connect(left, options: notificationConnectionOptions)
|
|
4284
|
-
centralManager?.connect(right, options: notificationConnectionOptions)
|
|
4285
|
-
return true
|
|
4320
|
+
peripheral.delegate = self
|
|
4321
|
+
centralManager?.connect(peripheral, options: notificationConnectionOptions)
|
|
4286
4322
|
}
|
|
4287
4323
|
|
|
4288
4324
|
private func getConnectedDevices() -> [CBPeripheral] {
|
|
@@ -4628,6 +4664,7 @@ class G2: NSObject, SGCManager {
|
|
|
4628
4664
|
}
|
|
4629
4665
|
|
|
4630
4666
|
private func setFullyConnected() {
|
|
4667
|
+
guard connectionSession.isReady, !isDisconnecting else { return }
|
|
4631
4668
|
let isFullyConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
|
|
4632
4669
|
let isFullyBooted = DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
|
|
4633
4670
|
if !isFullyConnected {
|
|
@@ -4683,7 +4720,10 @@ class G2: NSObject, SGCManager {
|
|
|
4683
4720
|
return (x, y, z)
|
|
4684
4721
|
}
|
|
4685
4722
|
|
|
4686
|
-
|
|
4723
|
+
func handleTouchEvent(_ devEventData: Data) {
|
|
4724
|
+
// A single arm can emit system_exit and gestures before the pair has authenticated.
|
|
4725
|
+
// Those events must neither establish readiness nor drive page recovery.
|
|
4726
|
+
guard connectionSession.isReady, !isDisconnecting else { return }
|
|
4687
4727
|
// Parse SendDeviceEvent: field 1=ListEvent, field 2=TextEvent, field 3=SysEvent
|
|
4688
4728
|
var reader = ProtobufReader(devEventData)
|
|
4689
4729
|
let fields = reader.parseFields()
|
|
@@ -4719,9 +4759,6 @@ class G2: NSObject, SGCManager {
|
|
|
4719
4759
|
}
|
|
4720
4760
|
lastClickTimestamp = timestamp
|
|
4721
4761
|
|
|
4722
|
-
// if we are receiving touch events we are fully booted:
|
|
4723
|
-
setFullyConnected()
|
|
4724
|
-
|
|
4725
4762
|
// Bridge.log("G2: handleTouchEvent: \(fields)")
|
|
4726
4763
|
// Bridge.log(
|
|
4727
4764
|
// "G2: handleTouchEvent: \(devEventData.map { String(format: "%02X", $0) }.joined())")
|
|
@@ -4970,16 +5007,13 @@ class G2: NSObject, SGCManager {
|
|
|
4970
5007
|
}
|
|
4971
5008
|
let secAuthStr = secAuth.map { $0 ? "true" : "false" } ?? "?"
|
|
4972
5009
|
Bridge.log("G2: Authentication response: \(sourceKey) secAuth=\(secAuthStr)")
|
|
4973
|
-
if secAuth ==
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
Bridge.log("G2: Both sides authenticated, setting fully booted and connected")
|
|
4981
|
-
setFullyConnected()
|
|
4982
|
-
}
|
|
5010
|
+
if secAuth == false {
|
|
5011
|
+
recoverConnection()
|
|
5012
|
+
return
|
|
5013
|
+
}
|
|
5014
|
+
if let side = G2ConnectionSession.Side(rawValue: sourceKey) {
|
|
5015
|
+
connectionSession.authenticated(side, success: secAuth == true)
|
|
5016
|
+
finishConnectionIfReady()
|
|
4983
5017
|
}
|
|
4984
5018
|
}
|
|
4985
5019
|
}
|
|
@@ -5205,14 +5239,18 @@ extension G2: CBCentralManagerDelegate {
|
|
|
5205
5239
|
DispatchQueue.main.async { [weak self] in
|
|
5206
5240
|
guard let self = self else { return }
|
|
5207
5241
|
Bridge.log("G2: Bluetooth state: \(state.rawValue)")
|
|
5242
|
+
guard !self.isDisconnecting else { return }
|
|
5208
5243
|
if state == .poweredOn {
|
|
5209
5244
|
_ = self.startScan()
|
|
5245
|
+
} else {
|
|
5246
|
+
self.reconnectionManager.stop()
|
|
5247
|
+
self.resetConnection()
|
|
5210
5248
|
}
|
|
5211
5249
|
}
|
|
5212
5250
|
}
|
|
5213
5251
|
|
|
5214
5252
|
nonisolated func centralManager(
|
|
5215
|
-
_
|
|
5253
|
+
_: CBCentralManager,
|
|
5216
5254
|
didDiscover peripheral: CBPeripheral,
|
|
5217
5255
|
advertisementData: [String: Any],
|
|
5218
5256
|
rssi _: NSNumber
|
|
@@ -5235,6 +5273,7 @@ extension G2: CBCentralManagerDelegate {
|
|
|
5235
5273
|
Bridge.log("G2: Could not extract SN from manufacturer data")
|
|
5236
5274
|
return
|
|
5237
5275
|
}
|
|
5276
|
+
guard !self.isDisconnecting else { return }
|
|
5238
5277
|
// sn = "S200LACA040040"
|
|
5239
5278
|
let mfgHex = mfgData.map { String(format: "%02X", $0) }.joined(separator: " ")
|
|
5240
5279
|
Bridge.log(
|
|
@@ -5257,33 +5296,17 @@ extension G2: CBCentralManagerDelegate {
|
|
|
5257
5296
|
self.emitDiscoveredDevice(serialNumber)
|
|
5258
5297
|
|
|
5259
5298
|
// If scan-only mode (no search ID set), don't auto-connect
|
|
5260
|
-
guard
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
self.leftPeripheral = peripheral
|
|
5270
|
-
peripheral.delegate = self
|
|
5271
|
-
central.connect(peripheral, options: self.notificationConnectionOptions)
|
|
5272
|
-
// Bridge.log("G2: Connecting to LEFT: \(name)")
|
|
5273
|
-
}
|
|
5274
|
-
} else if name.contains("_R_") {
|
|
5275
|
-
if self.rightPeripheral == nil {
|
|
5276
|
-
self.rightPeripheral = peripheral
|
|
5277
|
-
peripheral.delegate = self
|
|
5278
|
-
central.connect(peripheral, options: self.notificationConnectionOptions)
|
|
5279
|
-
// Bridge.log("G2: Connecting to RIGHT: \(name)")
|
|
5280
|
-
}
|
|
5281
|
-
}
|
|
5282
|
-
|
|
5283
|
-
// Stop scanning once we have both
|
|
5299
|
+
guard let side = G2ConnectionTarget.side(
|
|
5300
|
+
identifier: peripheral.identifier, name: name, advertisedSerial: serialNumber,
|
|
5301
|
+
searchID: self.DEVICE_SEARCH_ID,
|
|
5302
|
+
leftUUID: self.leftGlassUUID(forSN: self.DEVICE_SEARCH_ID),
|
|
5303
|
+
rightUUID: self.rightGlassUUID(forSN: self.DEVICE_SEARCH_ID)
|
|
5304
|
+
) else { return }
|
|
5305
|
+
self.connectPeripheral(peripheral, side: side)
|
|
5306
|
+
|
|
5307
|
+
// Keep the attempt deadline until both arms are authenticated, not merely discovered.
|
|
5284
5308
|
if self.leftPeripheral != nil && self.rightPeripheral != nil {
|
|
5285
5309
|
self.stopScan()
|
|
5286
|
-
self.cancelPairingTimeout()
|
|
5287
5310
|
}
|
|
5288
5311
|
}
|
|
5289
5312
|
}
|
|
@@ -5300,7 +5323,10 @@ extension G2: CBCentralManagerDelegate {
|
|
|
5300
5323
|
nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
5301
5324
|
DispatchQueue.main.async { [weak self] in
|
|
5302
5325
|
guard let self = self else { return }
|
|
5326
|
+
guard peripheral === self.leftPeripheral || peripheral === self.rightPeripheral,
|
|
5327
|
+
!self.isDisconnecting else { return }
|
|
5303
5328
|
Bridge.log("G2: Connected to \(peripheral.name ?? "unknown")")
|
|
5329
|
+
self.updatePartialConnection()
|
|
5304
5330
|
|
|
5305
5331
|
// Store UUID for reconnection, keyed by serial number.
|
|
5306
5332
|
let sn = peripheral.name.flatMap { self.deviceNameToSerialNumber[$0] }
|
|
@@ -5330,59 +5356,29 @@ extension G2: CBCentralManagerDelegate {
|
|
|
5330
5356
|
Bridge.log("G2: Disconnected \(side): \(error?.localizedDescription ?? "clean")")
|
|
5331
5357
|
|
|
5332
5358
|
guard peripheral === self.leftPeripheral || peripheral === self.rightPeripheral else { return }
|
|
5333
|
-
self.notificationEpoch += 1
|
|
5334
|
-
self.notificationControlMagic = nil
|
|
5335
|
-
// Only reconnect if not intentionally disconnecting
|
|
5336
5359
|
if self.isDisconnecting { return }
|
|
5360
|
+
self.recoverConnection()
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5337
5363
|
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
self.
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
self.rightWriteChar = nil
|
|
5347
|
-
self.leftNotifyChar = nil
|
|
5348
|
-
self.rightNotifyChar = nil
|
|
5349
|
-
self.leftAudioChar = nil
|
|
5350
|
-
self.rightAudioChar = nil
|
|
5351
|
-
self.authStarted = false
|
|
5352
|
-
|
|
5353
|
-
self.startupPageCreated = false
|
|
5354
|
-
self.pageCreated = false
|
|
5355
|
-
self.dashboardShowing = 0
|
|
5356
|
-
self.dashboardOpening = false
|
|
5357
|
-
DeviceStore.shared.apply("glasses", "connected", false)
|
|
5358
|
-
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
5359
|
-
self.publishNotificationStatus()
|
|
5360
|
-
|
|
5361
|
-
// Start persistent reconnection loop (every 30s, unlimited attempts)
|
|
5362
|
-
self.startReconnectionTimer()
|
|
5364
|
+
nonisolated func centralManager(
|
|
5365
|
+
_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?
|
|
5366
|
+
) {
|
|
5367
|
+
Task { @MainActor [weak self] in
|
|
5368
|
+
guard let self, !self.isDisconnecting,
|
|
5369
|
+
peripheral === self.leftPeripheral || peripheral === self.rightPeripheral else { return }
|
|
5370
|
+
Bridge.log("G2: Connection failed: \(error?.localizedDescription ?? "unknown")")
|
|
5371
|
+
// The attempt deadline bounds failures without an immediate reconnect loop.
|
|
5363
5372
|
}
|
|
5364
5373
|
}
|
|
5365
5374
|
|
|
5366
5375
|
private func startReconnectionTimer() {
|
|
5367
|
-
|
|
5368
|
-
await
|
|
5369
|
-
guard let self else { return
|
|
5370
|
-
|
|
5371
|
-
// Check if already connected
|
|
5372
|
-
if await MainActor.run(body: {
|
|
5373
|
-
DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
|
|
5374
|
-
}) {
|
|
5375
|
-
Bridge.log("G2: Already connected, stopping reconnection")
|
|
5376
|
-
return true
|
|
5377
|
-
}
|
|
5378
|
-
|
|
5376
|
+
reconnectionManager.start { [weak self] in
|
|
5377
|
+
await MainActor.run {
|
|
5378
|
+
guard let self, !self.isDisconnecting else { return true }
|
|
5379
|
+
if self.connectionSession.isReady { return true }
|
|
5379
5380
|
Bridge.log("G2: Attempting reconnection...")
|
|
5380
|
-
|
|
5381
|
-
await MainActor.run {
|
|
5382
|
-
self.startScan()
|
|
5383
|
-
}
|
|
5384
|
-
|
|
5385
|
-
// Return false to keep trying
|
|
5381
|
+
self.startScan()
|
|
5386
5382
|
return false
|
|
5387
5383
|
}
|
|
5388
5384
|
}
|
|
@@ -5393,9 +5389,13 @@ extension G2: CBCentralManagerDelegate {
|
|
|
5393
5389
|
|
|
5394
5390
|
extension G2: CBPeripheralDelegate {
|
|
5395
5391
|
nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5392
|
+
Task { @MainActor [weak self] in
|
|
5393
|
+
guard let self, !self.isDisconnecting,
|
|
5394
|
+
peripheral === self.leftPeripheral || peripheral === self.rightPeripheral,
|
|
5395
|
+
let services = peripheral.services else { return }
|
|
5396
|
+
for service in services {
|
|
5397
|
+
peripheral.discoverCharacteristics(nil, for: service)
|
|
5398
|
+
}
|
|
5399
5399
|
}
|
|
5400
5400
|
}
|
|
5401
5401
|
|
|
@@ -5407,6 +5407,8 @@ extension G2: CBPeripheralDelegate {
|
|
|
5407
5407
|
|
|
5408
5408
|
DispatchQueue.main.async { [weak self] in
|
|
5409
5409
|
guard let self = self else { return }
|
|
5410
|
+
guard !self.isDisconnecting,
|
|
5411
|
+
peripheral === self.leftPeripheral || peripheral === self.rightPeripheral else { return }
|
|
5410
5412
|
let side = peripheral === self.leftPeripheral ? "LEFT" : "RIGHT"
|
|
5411
5413
|
|
|
5412
5414
|
for char in characteristics {
|
|
@@ -5448,23 +5450,32 @@ extension G2: CBPeripheralDelegate {
|
|
|
5448
5450
|
}
|
|
5449
5451
|
}
|
|
5450
5452
|
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
Bridge.log("G2: LEFT initialized")
|
|
5455
|
-
} else if peripheral === self.rightPeripheral && self.rightWriteChar != nil
|
|
5456
|
-
&& self.rightNotifyChar != nil
|
|
5457
|
-
{
|
|
5458
|
-
self.rightInitialized = true
|
|
5459
|
-
Bridge.log("G2: RIGHT initialized")
|
|
5460
|
-
}
|
|
5453
|
+
self.updateInitialization(for: peripheral)
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5461
5456
|
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5457
|
+
private func updateInitialization(for peripheral: CBPeripheral) {
|
|
5458
|
+
guard !isDisconnecting, peripheral.state == .connected else { return }
|
|
5459
|
+
if peripheral === leftPeripheral, leftWriteChar != nil, leftNotifyChar?.isNotifying == true {
|
|
5460
|
+
connectionSession.initialize(.left)
|
|
5461
|
+
} else if peripheral === rightPeripheral, rightWriteChar != nil, rightNotifyChar?.isNotifying == true {
|
|
5462
|
+
connectionSession.initialize(.right)
|
|
5463
|
+
}
|
|
5464
|
+
startAuthenticationIfReady()
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
nonisolated func peripheral(
|
|
5468
|
+
_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?
|
|
5469
|
+
) {
|
|
5470
|
+
guard characteristic.uuid == G2BLE.CHAR_NOTIFY else { return }
|
|
5471
|
+
Task { @MainActor [weak self] in
|
|
5472
|
+
guard let self, !self.isDisconnecting,
|
|
5473
|
+
peripheral === self.leftPeripheral || peripheral === self.rightPeripheral else { return }
|
|
5474
|
+
guard error == nil, characteristic.isNotifying else {
|
|
5475
|
+
self.recoverConnection()
|
|
5476
|
+
return
|
|
5467
5477
|
}
|
|
5478
|
+
self.updateInitialization(for: peripheral)
|
|
5468
5479
|
}
|
|
5469
5480
|
}
|
|
5470
5481
|
|
|
@@ -5485,7 +5496,10 @@ extension G2: CBPeripheralDelegate {
|
|
|
5485
5496
|
Task { @MainActor [weak self] in
|
|
5486
5497
|
guard let self = self else { return }
|
|
5487
5498
|
|
|
5499
|
+
guard !self.isDisconnecting,
|
|
5500
|
+
peripheral === self.leftPeripheral || peripheral === self.rightPeripheral else { return }
|
|
5488
5501
|
if characteristic.uuid == G2BLE.AUDIO_NOTIFY {
|
|
5502
|
+
guard self.connectionSession.isReady else { return }
|
|
5489
5503
|
// Audio data - forward to mic system
|
|
5490
5504
|
await self.handleAudioData(data)
|
|
5491
5505
|
} else if characteristic.uuid == G2BLE.CHAR_NOTIFY {
|