Bridge: advertise node capabilities
This commit is contained in:
@@ -178,6 +178,10 @@ class NodeRuntime(context: Context) {
|
||||
val resolved =
|
||||
if (storedToken.isNullOrBlank()) {
|
||||
_statusText.value = "Pairing…"
|
||||
val caps = buildList {
|
||||
add("canvas")
|
||||
if (cameraEnabled.value) add("camera")
|
||||
}
|
||||
BridgePairingClient().pairAndHello(
|
||||
endpoint = endpoint,
|
||||
hello =
|
||||
@@ -187,6 +191,7 @@ class NodeRuntime(context: Context) {
|
||||
token = null,
|
||||
platform = "Android",
|
||||
version = "dev",
|
||||
caps = caps,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
@@ -209,6 +214,11 @@ class NodeRuntime(context: Context) {
|
||||
token = authToken,
|
||||
platform = "Android",
|
||||
version = "dev",
|
||||
caps =
|
||||
buildList {
|
||||
add("canvas")
|
||||
if (cameraEnabled.value) add("camera")
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.steipete.clawdis.node.bridge
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
@@ -24,6 +25,7 @@ class BridgePairingClient {
|
||||
val token: String?,
|
||||
val platform: String?,
|
||||
val version: String?,
|
||||
val caps: List<String>?,
|
||||
)
|
||||
|
||||
data class PairResult(val ok: Boolean, val token: String?, val error: String? = null)
|
||||
@@ -55,6 +57,7 @@ class BridgePairingClient {
|
||||
hello.token?.let { put("token", JsonPrimitive(it)) }
|
||||
hello.platform?.let { put("platform", JsonPrimitive(it)) }
|
||||
hello.version?.let { put("version", JsonPrimitive(it)) }
|
||||
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
||||
},
|
||||
)
|
||||
|
||||
@@ -76,6 +79,7 @@ class BridgePairingClient {
|
||||
hello.displayName?.let { put("displayName", JsonPrimitive(it)) }
|
||||
hello.platform?.let { put("platform", JsonPrimitive(it)) }
|
||||
hello.version?.let { put("version", JsonPrimitive(it)) }
|
||||
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
@@ -39,6 +40,7 @@ class BridgeSession(
|
||||
val token: String?,
|
||||
val platform: String?,
|
||||
val version: String?,
|
||||
val caps: List<String>?,
|
||||
)
|
||||
|
||||
data class InvokeRequest(val id: String, val command: String, val paramsJson: String?)
|
||||
@@ -191,6 +193,7 @@ class BridgeSession(
|
||||
hello.token?.let { put("token", JsonPrimitive(it)) }
|
||||
hello.platform?.let { put("platform", JsonPrimitive(it)) }
|
||||
hello.version?.let { put("version", JsonPrimitive(it)) }
|
||||
hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) }
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ actor BridgeClient {
|
||||
platform: hello.platform,
|
||||
version: hello.version,
|
||||
deviceFamily: hello.deviceFamily,
|
||||
modelIdentifier: hello.modelIdentifier),
|
||||
modelIdentifier: hello.modelIdentifier,
|
||||
caps: hello.caps),
|
||||
over: connection)
|
||||
|
||||
onStatus?("Waiting for approval…")
|
||||
|
||||
@@ -134,7 +134,21 @@ final class BridgeConnectionController {
|
||||
platform: self.platformString(),
|
||||
version: self.appVersion(),
|
||||
deviceFamily: self.deviceFamily(),
|
||||
modelIdentifier: self.modelIdentifier())
|
||||
modelIdentifier: self.modelIdentifier(),
|
||||
caps: self.currentCaps())
|
||||
}
|
||||
|
||||
private func currentCaps() -> [String] {
|
||||
var caps: [String] = ["canvas"]
|
||||
|
||||
// Default-on: if the key doesn't exist yet, treat it as enabled.
|
||||
let cameraEnabled =
|
||||
UserDefaults.standard.object(forKey: "camera.enabled") == nil
|
||||
? true
|
||||
: UserDefaults.standard.bool(forKey: "camera.enabled")
|
||||
if cameraEnabled { caps.append("camera") }
|
||||
|
||||
return caps
|
||||
}
|
||||
|
||||
private func platformString() -> String {
|
||||
|
||||
@@ -3,6 +3,15 @@ import Foundation
|
||||
import Network
|
||||
import OSLog
|
||||
|
||||
struct BridgeNodeInfo: Sendable {
|
||||
var nodeId: String
|
||||
var displayName: String?
|
||||
var platform: String?
|
||||
var version: String?
|
||||
var remoteAddress: String?
|
||||
var caps: [String]?
|
||||
}
|
||||
|
||||
actor BridgeConnectionHandler {
|
||||
private let connection: NWConnection
|
||||
private let logger: Logger
|
||||
@@ -38,7 +47,7 @@ actor BridgeConnectionHandler {
|
||||
var serverName: String
|
||||
var resolveAuth: @Sendable (BridgeHello) async -> AuthResult
|
||||
var handlePair: @Sendable (BridgePairRequest) async -> PairResult
|
||||
var onAuthenticated: (@Sendable (String) async -> Void)?
|
||||
var onAuthenticated: (@Sendable (BridgeNodeInfo) async -> Void)?
|
||||
var onEvent: (@Sendable (String, BridgeEventFrame) async -> Void)?
|
||||
var onRequest: (@Sendable (String, BridgeRPCRequest) async -> BridgeRPCResponse)?
|
||||
}
|
||||
@@ -46,7 +55,7 @@ actor BridgeConnectionHandler {
|
||||
func run(
|
||||
resolveAuth: @escaping @Sendable (BridgeHello) async -> AuthResult,
|
||||
handlePair: @escaping @Sendable (BridgePairRequest) async -> PairResult,
|
||||
onAuthenticated: (@Sendable (String) async -> Void)? = nil,
|
||||
onAuthenticated: (@Sendable (BridgeNodeInfo) async -> Void)? = nil,
|
||||
onDisconnected: (@Sendable (String) async -> Void)? = nil,
|
||||
onEvent: (@Sendable (String, BridgeEventFrame) async -> Void)? = nil,
|
||||
onRequest: (@Sendable (String, BridgeRPCRequest) async -> BridgeRPCResponse)? = nil) async
|
||||
@@ -125,11 +134,19 @@ actor BridgeConnectionHandler {
|
||||
{
|
||||
do {
|
||||
let hello = try self.decoder.decode(BridgeHello.self, from: data)
|
||||
self.nodeId = hello.nodeId
|
||||
let nodeId = hello.nodeId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.nodeId = nodeId
|
||||
let result = await context.resolveAuth(hello)
|
||||
await self.handleAuthResult(result, serverName: context.serverName)
|
||||
if case .ok = result, let nodeId = self.nodeId {
|
||||
await context.onAuthenticated?(nodeId)
|
||||
if case .ok = result {
|
||||
await context.onAuthenticated?(
|
||||
BridgeNodeInfo(
|
||||
nodeId: nodeId,
|
||||
displayName: hello.displayName,
|
||||
platform: hello.platform,
|
||||
version: hello.version,
|
||||
remoteAddress: self.remoteAddressString(),
|
||||
caps: hello.caps))
|
||||
}
|
||||
} catch {
|
||||
await self.sendError(code: "INVALID_REQUEST", message: error.localizedDescription)
|
||||
@@ -142,18 +159,27 @@ actor BridgeConnectionHandler {
|
||||
{
|
||||
do {
|
||||
let req = try self.decoder.decode(BridgePairRequest.self, from: data)
|
||||
self.nodeId = req.nodeId
|
||||
let nodeId = req.nodeId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.nodeId = nodeId
|
||||
let enriched = BridgePairRequest(
|
||||
type: req.type,
|
||||
nodeId: req.nodeId,
|
||||
nodeId: nodeId,
|
||||
displayName: req.displayName,
|
||||
platform: req.platform,
|
||||
version: req.version,
|
||||
caps: req.caps,
|
||||
remoteAddress: self.remoteAddressString())
|
||||
let result = await context.handlePair(enriched)
|
||||
await self.handlePairResult(result, serverName: context.serverName)
|
||||
if case .ok = result, let nodeId = self.nodeId {
|
||||
await context.onAuthenticated?(nodeId)
|
||||
if case .ok = result {
|
||||
await context.onAuthenticated?(
|
||||
BridgeNodeInfo(
|
||||
nodeId: nodeId,
|
||||
displayName: enriched.displayName,
|
||||
platform: enriched.platform,
|
||||
version: enriched.version,
|
||||
remoteAddress: enriched.remoteAddress,
|
||||
caps: enriched.caps))
|
||||
}
|
||||
} catch {
|
||||
await self.sendError(code: "INVALID_REQUEST", message: error.localizedDescription)
|
||||
|
||||
@@ -13,6 +13,7 @@ actor BridgeServer {
|
||||
private var isRunning = false
|
||||
private var store: PairedNodesStore?
|
||||
private var connections: [String: BridgeConnectionHandler] = [:]
|
||||
private var nodeInfoById: [String: BridgeNodeInfo] = [:]
|
||||
private var presenceTasks: [String: Task<Void, Never>] = [:]
|
||||
private var chatSubscriptions: [String: Set<String>] = [:]
|
||||
private var gatewayPushTask: Task<Void, Never>?
|
||||
@@ -73,7 +74,7 @@ actor BridgeServer {
|
||||
}
|
||||
|
||||
private func handle(connection: NWConnection) async {
|
||||
let handler = BridgeConnectionHandler(connection: connection, logger: self.logger)
|
||||
let handler = BridgeConnectionHandler(connection: connection, logger: self.logger)
|
||||
await handler.run(
|
||||
resolveAuth: { [weak self] hello in
|
||||
await self?.authorize(hello: hello) ?? .error(code: "UNAVAILABLE", message: "bridge unavailable")
|
||||
@@ -81,8 +82,8 @@ actor BridgeServer {
|
||||
handlePair: { [weak self] request in
|
||||
await self?.pair(request: request) ?? .error(code: "UNAVAILABLE", message: "bridge unavailable")
|
||||
},
|
||||
onAuthenticated: { [weak self] nodeId in
|
||||
await self?.registerConnection(handler: handler, nodeId: nodeId)
|
||||
onAuthenticated: { [weak self] node in
|
||||
await self?.registerConnection(handler: handler, node: node)
|
||||
},
|
||||
onDisconnected: { [weak self] nodeId in
|
||||
await self?.unregisterConnection(nodeId: nodeId)
|
||||
@@ -112,10 +113,22 @@ actor BridgeServer {
|
||||
Array(self.connections.keys).sorted()
|
||||
}
|
||||
|
||||
private func registerConnection(handler: BridgeConnectionHandler, nodeId: String) async {
|
||||
self.connections[nodeId] = handler
|
||||
await self.beaconPresence(nodeId: nodeId, reason: "connect")
|
||||
self.startPresenceTask(nodeId: nodeId)
|
||||
func connectedNodes() -> [BridgeNodeInfo] {
|
||||
self.nodeInfoById.values.sorted { a, b in
|
||||
(a.displayName ?? a.nodeId) < (b.displayName ?? b.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
func pairedNodes() async -> [PairedNode] {
|
||||
guard let store = self.store else { return [] }
|
||||
return await store.all()
|
||||
}
|
||||
|
||||
private func registerConnection(handler: BridgeConnectionHandler, node: BridgeNodeInfo) async {
|
||||
self.connections[node.nodeId] = handler
|
||||
self.nodeInfoById[node.nodeId] = node
|
||||
await self.beaconPresence(nodeId: node.nodeId, reason: "connect")
|
||||
self.startPresenceTask(nodeId: node.nodeId)
|
||||
self.ensureGatewayPushTask()
|
||||
}
|
||||
|
||||
@@ -123,6 +136,7 @@ actor BridgeServer {
|
||||
await self.beaconPresence(nodeId: nodeId, reason: "disconnect")
|
||||
self.stopPresenceTask(nodeId: nodeId)
|
||||
self.connections.removeValue(forKey: nodeId)
|
||||
self.nodeInfoById.removeValue(forKey: nodeId)
|
||||
self.chatSubscriptions[nodeId] = nil
|
||||
self.stopGatewayPushTaskIfIdle()
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ public struct BridgeHello: Codable, Sendable {
|
||||
public let version: String?
|
||||
public let deviceFamily: String?
|
||||
public let modelIdentifier: String?
|
||||
public let caps: [String]?
|
||||
|
||||
public init(
|
||||
type: String = "hello",
|
||||
@@ -74,7 +75,8 @@ public struct BridgeHello: Codable, Sendable {
|
||||
platform: String?,
|
||||
version: String?,
|
||||
deviceFamily: String? = nil,
|
||||
modelIdentifier: String? = nil)
|
||||
modelIdentifier: String? = nil,
|
||||
caps: [String]? = nil)
|
||||
{
|
||||
self.type = type
|
||||
self.nodeId = nodeId
|
||||
@@ -84,6 +86,7 @@ public struct BridgeHello: Codable, Sendable {
|
||||
self.version = version
|
||||
self.deviceFamily = deviceFamily
|
||||
self.modelIdentifier = modelIdentifier
|
||||
self.caps = caps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +108,7 @@ public struct BridgePairRequest: Codable, Sendable {
|
||||
public let version: String?
|
||||
public let deviceFamily: String?
|
||||
public let modelIdentifier: String?
|
||||
public let caps: [String]?
|
||||
public let remoteAddress: String?
|
||||
|
||||
public init(
|
||||
@@ -115,6 +119,7 @@ public struct BridgePairRequest: Codable, Sendable {
|
||||
version: String?,
|
||||
deviceFamily: String? = nil,
|
||||
modelIdentifier: String? = nil,
|
||||
caps: [String]? = nil,
|
||||
remoteAddress: String? = nil)
|
||||
{
|
||||
self.type = type
|
||||
@@ -124,6 +129,7 @@ public struct BridgePairRequest: Codable, Sendable {
|
||||
self.version = version
|
||||
self.deviceFamily = deviceFamily
|
||||
self.modelIdentifier = modelIdentifier
|
||||
self.caps = caps
|
||||
self.remoteAddress = remoteAddress
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user