Integrate live phone and watch remote prototype with guarded motion control
This commit is contained in:
@@ -19,7 +19,8 @@ public final class WatchRelayBridge: NSObject {
|
||||
public var onStateChange: ((State) -> Void)?
|
||||
|
||||
private let session: any RelaySessionSending
|
||||
private var grant: RemoteSessionGrant?
|
||||
public private(set) var grant: RemoteSessionGrant?
|
||||
private var generation = UUID()
|
||||
private var nextSequence: UInt64 = 0
|
||||
private var activationReady = false
|
||||
private var pendingReplies: [UUID: RelayReplyOnce] = [:]
|
||||
@@ -46,7 +47,9 @@ public final class WatchRelayBridge: NSObject {
|
||||
}
|
||||
|
||||
public func reachabilityDidChange() {
|
||||
guard activationReady, grant == nil else { return }
|
||||
guard activationReady else { return }
|
||||
if !session.isReachable { invalidatePending() }
|
||||
if grant != nil { return }
|
||||
state = .ready(reachable: session.isReachable)
|
||||
}
|
||||
|
||||
@@ -65,10 +68,13 @@ public final class WatchRelayBridge: NSObject {
|
||||
}
|
||||
|
||||
public func beginSession() {
|
||||
guard grant == nil, state != .sessionNegotiating else { return }
|
||||
guard activationReady, session.isReachable, grant == nil else {
|
||||
state = .unavailable
|
||||
return
|
||||
}
|
||||
invalidatePending()
|
||||
let requestGeneration = generation
|
||||
state = .sessionNegotiating
|
||||
let requestID = UUID()
|
||||
let request = BeginRemoteSessionRequest()
|
||||
@@ -77,7 +83,7 @@ public final class WatchRelayBridge: NSObject {
|
||||
return
|
||||
}
|
||||
send(message: message, requestID: requestID) { [weak self] response in
|
||||
guard let self else { return }
|
||||
guard let self, self.generation == requestGeneration, self.session.isReachable else { return }
|
||||
guard let container = try? RelayContainer(dictionary: response),
|
||||
container.kind == .session,
|
||||
let received = try? container.decode(RemoteSessionGrant.self,
|
||||
@@ -93,33 +99,34 @@ public final class WatchRelayBridge: NSObject {
|
||||
}
|
||||
|
||||
public func endSession() {
|
||||
guard activationReady, let grant else { return }
|
||||
let request = EndRemoteSessionRequest(sessionID: grant.sessionID)
|
||||
guard let message = try? RelayContainer(kind: .endSession, value: request).dictionary() else {
|
||||
self.grant = nil
|
||||
state = .ready(reachable: session.isReachable)
|
||||
return
|
||||
}
|
||||
let requestID = UUID()
|
||||
send(message: message, requestID: requestID) { [weak self] response in
|
||||
guard let self else { return }
|
||||
guard self.grant?.sessionID == grant.sessionID else { return }
|
||||
self.grant = nil
|
||||
self.nextSequence = 0
|
||||
self.state = .ready(reachable: self.session.isReachable)
|
||||
let oldGrant = grant
|
||||
invalidatePending()
|
||||
state = activationReady ? .ready(reachable: session.isReachable) : .inactive
|
||||
guard activationReady, session.isReachable, let oldGrant,
|
||||
let message = try? RelayContainer(kind: .endSession, value: EndRemoteSessionRequest(sessionID: oldGrant.sessionID)).dictionary() else { return }
|
||||
// Locally revoke immediately; a delayed end reply cannot revoke a newer grant.
|
||||
send(message: message, requestID: UUID()) { _ in }
|
||||
}
|
||||
|
||||
public func sendCommandResult(_ command: RemoteCommand) async -> Result<CommandReceipt, RelayFailure> {
|
||||
await withCheckedContinuation { continuation in
|
||||
sendCommand(command) { continuation.resume(returning: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// One command is one live sendMessage. A sequence is consumed before the
|
||||
/// send; ambiguous timeout/failure is never replayed.
|
||||
public func sendCommand(_ command: RemoteCommand, now: Date = Date()) {
|
||||
public func sendCommand(_ command: RemoteCommand, now: Date = Date(),
|
||||
completion: @escaping @MainActor (Result<CommandReceipt, RelayFailure>) -> Void = { _ in }) {
|
||||
guard activationReady, session.isReachable, let grant else {
|
||||
state = .unavailable
|
||||
completion(.failure(.init(code: .unavailable)))
|
||||
return
|
||||
}
|
||||
guard nextSequence < UInt64.max else {
|
||||
self.grant = nil
|
||||
state = .unavailable
|
||||
completion(.failure(.init(code: .sessionInvalidated)))
|
||||
return
|
||||
}
|
||||
let sequence = nextSequence
|
||||
@@ -132,17 +139,20 @@ public final class WatchRelayBridge: NSObject {
|
||||
expiresAt: now.addingTimeInterval(lifetime),
|
||||
command: command)
|
||||
guard let message = try? RelayContainer(kind: .command, value: envelope).dictionary() else {
|
||||
completion(.failure(.init(code: .malformedRequest)))
|
||||
return
|
||||
}
|
||||
let requestGeneration = generation
|
||||
send(message: message, requestID: UUID()) { [weak self] response in
|
||||
guard let self,
|
||||
guard let self, self.generation == requestGeneration,
|
||||
let container = try? RelayContainer(dictionary: response),
|
||||
container.kind == .receipt,
|
||||
let receipt = try? container.decode(CommandReceipt.self,
|
||||
expectedKind: .receipt),
|
||||
receipt.id == envelope.id else { return }
|
||||
receipt.id == envelope.id else { completion(.failure(.init(code: .unavailable))); return }
|
||||
completion(.success(receipt))
|
||||
// A receipt is command feedback, not evidence that the TV changed.
|
||||
if receipt.status == .unavailable { self.state = .unavailable }
|
||||
if receipt.status != .sentToTransport { self.invalidatePending(); self.state = .unavailable }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,12 +160,12 @@ public final class WatchRelayBridge: NSObject {
|
||||
response: @escaping @MainActor ([String: Any]) -> Void) {
|
||||
guard pendingReplies.count < 32 else {
|
||||
state = .unavailable
|
||||
response(relayErrorReply(RelayFailure(code: .unavailable)))
|
||||
return
|
||||
}
|
||||
let once = RelayReplyOnce { [weak self] value in
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self, self.pendingReplies.removeValue(forKey: requestID) != nil else { return }
|
||||
guard self.activationReady else { return }
|
||||
response(value)
|
||||
}
|
||||
}
|
||||
@@ -172,8 +182,8 @@ public final class WatchRelayBridge: NSObject {
|
||||
}
|
||||
|
||||
private func invalidatePending() {
|
||||
let replies = pendingReplies.values
|
||||
pendingReplies.removeAll()
|
||||
generation = UUID()
|
||||
let replies = Array(pendingReplies.values)
|
||||
for reply in replies { reply.reply(relayErrorReply(RelayFailure(code: .sessionInvalidated))) }
|
||||
grant = nil
|
||||
nextSequence = 0
|
||||
|
||||
@@ -33,8 +33,12 @@ private final class WatchWCSessionSending: RelaySessionSending {
|
||||
@MainActor
|
||||
public final class WatchWCSessionDelegate: NSObject, WCSessionDelegate {
|
||||
public let bridge: WatchRelayBridge
|
||||
public var onContext: ((RelayApplicationContext) -> Void)?
|
||||
public private(set) var latestContext: RelayApplicationContext?
|
||||
private let session: WCSession
|
||||
|
||||
public init(session: WCSession = .default) {
|
||||
self.session = session
|
||||
bridge = WatchRelayBridge(session: WatchWCSessionSending(session: session))
|
||||
super.init()
|
||||
session.delegate = self
|
||||
@@ -48,6 +52,21 @@ public final class WatchWCSessionDelegate: NSObject, WCSessionDelegate {
|
||||
guard let self else { return }
|
||||
if case .inactive = self.bridge.state { } else { self.bridge.didDeactivate() }
|
||||
self.bridge.activationDidComplete(error: error)
|
||||
self.receiveContext(self.session.receivedApplicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
private func receiveContext(_ dictionary: [String: Any]) {
|
||||
guard let context = try? RelayApplicationContext(dictionary: dictionary) else { return }
|
||||
latestContext = context
|
||||
onContext?(context)
|
||||
}
|
||||
|
||||
nonisolated public func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
||||
guard let context = try? RelayApplicationContext(dictionary: applicationContext) else { return }
|
||||
Task { @MainActor [weak self] in
|
||||
self?.latestContext = context
|
||||
self?.onContext?(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user