Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Mos/Logi/Core/LogiDeviceSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,22 @@ class LogiDeviceSession {
return .takeover(slot)
}

/// 部分接收器不会在休眠设备恢复时发送 0x41; 离线 slot 的真实报文可作为连接证据.
static func receiverPeripheralActivityAction(
slot: UInt8,
wasConnected: Bool,
isDivertCandidate: Bool,
alreadyManaged: Bool
) -> ReceiverSlotConnectionAction {
guard !wasConnected else { return .ignore }
return receiverSlotConnectionAction(
slot: slot,
connected: true,
isDivertCandidate: isDivertCandidate,
alreadyManaged: alreadyManaged
)
}

#if DEBUG
internal static func receiverTargetIsConnectedForTests(
connectionMode: ConnectionMode,
Expand Down Expand Up @@ -1965,6 +1981,40 @@ class LogiDeviceSession {
}
}

/// 初始枚举时设备可能仍在休眠且之后不补发 0x41. 第一条真实 peripheral 报文将 slot
/// 标记为在线并直接复用现有 takeover 队列; 已在线 slot 的后续报文不会重复触发.
private func handleReceiverPeripheralActivity(slot: UInt8) -> Bool {
guard slot >= 1 && slot <= 6, handshakeComplete, receiverEnumPhase != 1 else { return false }

guard let idx = receiverPairedDevices.firstIndex(where: { $0.slot == slot }) else { return false }
let wasConnected = receiverPairedDevices[idx].isConnected
guard !wasConnected else { return false }

receiverPairedDevices[idx].isConnected = true
receiverPairedDevices[idx].lastError = nil

let device = receiverPairedDevices.first(where: { $0.slot == slot })
let bindingsExist = !LogiCenter.shared.registry.aggregatedCacheIsEmpty
let isDivertCandidate = !Self.receiverNonDivertDeviceTypes.contains(device?.deviceType ?? 0)
&& bindingsExist
let action = Self.receiverPeripheralActivityAction(
slot: slot,
wasConnected: wasConnected,
isDivertCandidate: isDivertCandidate,
alreadyManaged: receiverManagedSlots.contains(slot)
)
LogiDebugPanel.log(
"[\(deviceInfo.name)] Inferred slot \(slot) connection from peripheral activity; candidate=\(isDivertCandidate) -> \(action)"
)
NotificationCenter.default.post(name: LogiSessionManager.sessionChangedNotification, object: nil)

if case .takeover(let takeoverSlot) = action {
scheduleReceiverSlotTakeover(slot: takeoverSlot)
return true
}
return false
}

/// 去抖调度 takeover: 每次 0x41 连接重置定时器, 该 slot 稳定在线 debounce 窗口后才真正接管.
/// 疯狂快切期间连接/断开成对到达, 定时器被反复取消, 一次发现都不触发.
private func scheduleReceiverSlotTakeover(slot: UInt8) {
Expand Down Expand Up @@ -3187,6 +3237,12 @@ class LogiDeviceSession {
return
}

// 部分接收器不会为初始扫描时休眠的设备补发 0x41. 第一条真实报文足以证明 slot 在线;
// takeover 启动后丢弃该条尚无可靠 feature 映射的报文, 后续报文走正常路由.
if handleReceiverPeripheralActivity(slot: devIdx) {
return
}

// Peripheral(设备)报文按 report[1] 路由. 0xFF(register)/pending ping 已在上面处理.
// 当前游标 slot 直接处理; 其它已接管 slot scoped 处理; 未知/未接管 slot drop.
if devIdx >= 1 && devIdx <= 6 {
Expand Down
13 changes: 13 additions & 0 deletions MosTests/LogiReceiverConnectionStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,17 @@ final class LogiReceiverConnectionStateTests: XCTestCase {
.ignore
)
}

func testPeripheralActivityTakesOverPreviouslyOfflineSlotOnce() {
XCTAssertEqual(
LogiDeviceSession.receiverPeripheralActivityAction(
slot: 1, wasConnected: false, isDivertCandidate: true, alreadyManaged: false),
.takeover(1)
)
XCTAssertEqual(
LogiDeviceSession.receiverPeripheralActivityAction(
slot: 1, wasConnected: true, isDivertCandidate: true, alreadyManaged: false),
.ignore
)
}
}