Presence: add device identity fields

This commit is contained in:
Peter Steinberger
2025-12-17 21:17:51 +01:00
parent 6148f862b9
commit e9502768af
19 changed files with 206 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
package com.steipete.clawdis.node
import android.content.Context
import android.os.Build
import com.steipete.clawdis.node.bridge.BridgeDiscovery
import com.steipete.clawdis.node.bridge.BridgeEndpoint
import com.steipete.clawdis.node.bridge.BridgePairingClient
@@ -182,6 +183,8 @@ class NodeRuntime(context: Context) {
token = null,
platform = "Android",
version = "dev",
deviceFamily = "Android",
modelIdentifier = Build.MODEL,
),
)
} else {
@@ -204,6 +207,8 @@ class NodeRuntime(context: Context) {
token = authToken,
platform = "Android",
version = "dev",
deviceFamily = "Android",
modelIdentifier = Build.MODEL,
),
)
}

View File

@@ -24,6 +24,8 @@ class BridgePairingClient {
val token: String?,
val platform: String?,
val version: String?,
val deviceFamily: String?,
val modelIdentifier: String?,
)
data class PairResult(val ok: Boolean, val token: String?, val error: String? = null)
@@ -55,6 +57,8 @@ class BridgePairingClient {
hello.token?.let { put("token", JsonPrimitive(it)) }
hello.platform?.let { put("platform", JsonPrimitive(it)) }
hello.version?.let { put("version", JsonPrimitive(it)) }
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
},
)
@@ -76,6 +80,8 @@ class BridgePairingClient {
hello.displayName?.let { put("displayName", JsonPrimitive(it)) }
hello.platform?.let { put("platform", JsonPrimitive(it)) }
hello.version?.let { put("version", JsonPrimitive(it)) }
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
},
)

View File

@@ -39,6 +39,8 @@ class BridgeSession(
val token: String?,
val platform: String?,
val version: String?,
val deviceFamily: String?,
val modelIdentifier: String?,
)
data class InvokeRequest(val id: String, val command: String, val paramsJson: String?)
@@ -191,6 +193,8 @@ class BridgeSession(
hello.token?.let { put("token", JsonPrimitive(it)) }
hello.platform?.let { put("platform", JsonPrimitive(it)) }
hello.version?.let { put("version", JsonPrimitive(it)) }
hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) }
hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
},
)

View File

@@ -46,6 +46,8 @@ class BridgePairingClientTest {
token = "token-123",
platform = "Android",
version = "test",
deviceFamily = "Android",
modelIdentifier = "SM-X000",
),
)
assertTrue(res.ok)
@@ -91,6 +93,8 @@ class BridgePairingClientTest {
token = null,
platform = "Android",
version = "test",
deviceFamily = "Android",
modelIdentifier = "SM-X000",
),
)
assertTrue(res.ok)
@@ -98,4 +102,3 @@ class BridgePairingClientTest {
server.await()
}
}

View File

@@ -51,7 +51,9 @@ actor BridgeClient {
nodeId: hello.nodeId,
displayName: hello.displayName,
platform: hello.platform,
version: hello.version),
version: hello.version,
deviceFamily: hello.deviceFamily,
modelIdentifier: hello.modelIdentifier),
over: connection)
onStatus?("Waiting for approval…")

View File

@@ -1,8 +1,10 @@
import ClawdisKit
import Darwin
import Foundation
import Network
import Observation
import SwiftUI
import UIKit
@MainActor
@Observable
@@ -131,12 +133,43 @@ final class BridgeConnectionController {
displayName: displayName,
token: token,
platform: self.platformString(),
version: self.appVersion())
version: self.appVersion(),
deviceFamily: self.deviceFamily(),
modelIdentifier: self.modelIdentifier())
}
private func platformString() -> String {
let v = ProcessInfo.processInfo.operatingSystemVersion
return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
let name: String
switch UIDevice.current.userInterfaceIdiom {
case .pad:
name = "iPadOS"
case .phone:
name = "iOS"
default:
name = "iOS"
}
return "\(name) \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
}
private func deviceFamily() -> String {
switch UIDevice.current.userInterfaceIdiom {
case .pad:
return "iPad"
case .phone:
return "iPhone"
default:
return "iOS"
}
}
private func modelIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machine = withUnsafeBytes(of: &systemInfo.machine) { ptr in
String(decoding: ptr.prefix { $0 != 0 }, as: UTF8.self)
}
return machine.isEmpty ? "unknown" : machine
}
private func appVersion() -> String {

View File

@@ -192,7 +192,7 @@ actor GatewayChannelActor {
let clientName = InstanceIdentity.displayName
let reqId = UUID().uuidString
let client: [String: ProtoAnyCodable] = [
var client: [String: ProtoAnyCodable] = [
"name": ProtoAnyCodable(clientName),
"version": ProtoAnyCodable(
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"),
@@ -200,6 +200,10 @@ actor GatewayChannelActor {
"mode": ProtoAnyCodable("app"),
"instanceId": ProtoAnyCodable(InstanceIdentity.instanceId),
]
client["deviceFamily"] = ProtoAnyCodable("Mac")
if let model = InstanceIdentity.modelIdentifier {
client["modelIdentifier"] = ProtoAnyCodable(model)
}
var params: [String: ProtoAnyCodable] = [
"minProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),
"maxProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),

View File

@@ -1,3 +1,4 @@
import Darwin
import Foundation
enum InstanceIdentity {
@@ -30,4 +31,15 @@ enum InstanceIdentity {
}
return "clawdis-mac"
}()
static let modelIdentifier: String? = {
var size = 0
guard sysctlbyname("hw.model", nil, &size, nil, 0) == 0, size > 1 else { return nil }
var buffer = [CChar](repeating: 0, count: size)
guard sysctlbyname("hw.model", &buffer, &size, nil, 0) == 0 else { return nil }
let s = String(cString: buffer).trimmingCharacters(in: .whitespacesAndNewlines)
return s.isEmpty ? nil : s
}()
}

View File

@@ -70,6 +70,11 @@ struct InstancesSettings: View {
if let platform = inst.platform, let prettyPlatform = self.prettyPlatform(platform) {
self.label(icon: self.platformIcon(platform), text: prettyPlatform)
}
if let deviceText = self.deviceDescription(inst),
let deviceIcon = self.deviceIcon(inst)
{
self.label(icon: deviceIcon, text: deviceText)
}
self.label(icon: "clock", text: inst.lastInputDescription)
if let mode = inst.mode { self.label(icon: "network", text: mode) }
if let reason = inst.reason, !reason.isEmpty {
@@ -115,6 +120,29 @@ struct InstancesSettings: View {
}
}
private func deviceIcon(_ inst: InstanceInfo) -> String? {
let family = inst.deviceFamily?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if family.isEmpty { return nil }
switch family.lowercased() {
case "ipad":
return "ipad"
case "iphone":
return "iphone"
case "mac":
return "laptopcomputer"
default:
return "cpu"
}
}
private func deviceDescription(_ inst: InstanceInfo) -> String? {
let family = inst.deviceFamily?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let model = inst.modelIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !family.isEmpty, !model.isEmpty { return "\(family) (\(model))" }
if !model.isEmpty { return model }
return family.isEmpty ? nil : family
}
private func prettyPlatform(_ raw: String) -> String? {
let (prefix, version) = self.parsePlatform(raw)
if prefix.isEmpty { return nil }

View File

@@ -10,6 +10,8 @@ struct InstanceInfo: Identifiable, Codable {
let ip: String?
let version: String?
let platform: String?
let deviceFamily: String?
let modelIdentifier: String?
let lastInputSeconds: Int?
let mode: String?
let reason: String?
@@ -284,6 +286,8 @@ final class InstancesStore {
ip: entry.ip,
version: entry.version,
platform: entry.platform,
deviceFamily: entry.devicefamily,
modelIdentifier: entry.modelidentifier,
lastInputSeconds: entry.lastinputseconds,
mode: entry.mode,
reason: entry.reason,
@@ -308,6 +312,8 @@ extension InstancesStore {
ip: "10.0.0.12",
version: "1.2.3",
platform: "macos 26.2.0",
deviceFamily: "Mac",
modelIdentifier: "Mac16,6",
lastInputSeconds: 12,
mode: "local",
reason: "preview",
@@ -319,6 +325,8 @@ extension InstancesStore {
ip: "100.64.0.2",
version: "1.2.3",
platform: "linux 6.6.0",
deviceFamily: "Linux",
modelIdentifier: "x86_64",
lastInputSeconds: 45,
mode: "remote",
reason: "preview",

View File

@@ -45,6 +45,8 @@ final class PresenceReporter {
"version": AnyHashable(version),
"reason": AnyHashable(reason),
]
params["deviceFamily"] = AnyHashable("Mac")
if let model = InstanceIdentity.modelIdentifier { params["modelIdentifier"] = AnyHashable(model) }
if let lastInput { params["lastInputSeconds"] = AnyHashable(lastInput) }
do {
try await ControlChannel.shared.sendSystemEvent(text, params: params)

View File

@@ -168,6 +168,8 @@ public struct PresenceEntry: Codable {
public let ip: String?
public let version: String?
public let platform: String?
public let devicefamily: String?
public let modelidentifier: String?
public let mode: String?
public let lastinputseconds: Int?
public let reason: String?
@@ -181,6 +183,8 @@ public struct PresenceEntry: Codable {
ip: String?,
version: String?,
platform: String?,
devicefamily: String?,
modelidentifier: String?,
mode: String?,
lastinputseconds: Int?,
reason: String?,
@@ -193,6 +197,8 @@ public struct PresenceEntry: Codable {
self.ip = ip
self.version = version
self.platform = platform
self.devicefamily = devicefamily
self.modelidentifier = modelidentifier
self.mode = mode
self.lastinputseconds = lastinputseconds
self.reason = reason
@@ -206,6 +212,8 @@ public struct PresenceEntry: Codable {
case ip
case version
case platform
case devicefamily = "deviceFamily"
case modelidentifier = "modelIdentifier"
case mode
case lastinputseconds = "lastInputSeconds"
case reason

View File

@@ -63,6 +63,8 @@ public struct BridgeHello: Codable, Sendable {
public let token: String?
public let platform: String?
public let version: String?
public let deviceFamily: String?
public let modelIdentifier: String?
public init(
type: String = "hello",
@@ -70,7 +72,9 @@ public struct BridgeHello: Codable, Sendable {
displayName: String?,
token: String?,
platform: String?,
version: String?)
version: String?,
deviceFamily: String? = nil,
modelIdentifier: String? = nil)
{
self.type = type
self.nodeId = nodeId
@@ -78,6 +82,8 @@ public struct BridgeHello: Codable, Sendable {
self.token = token
self.platform = platform
self.version = version
self.deviceFamily = deviceFamily
self.modelIdentifier = modelIdentifier
}
}
@@ -97,6 +103,8 @@ public struct BridgePairRequest: Codable, Sendable {
public let displayName: String?
public let platform: String?
public let version: String?
public let deviceFamily: String?
public let modelIdentifier: String?
public let remoteAddress: String?
public init(
@@ -105,6 +113,8 @@ public struct BridgePairRequest: Codable, Sendable {
displayName: String?,
platform: String?,
version: String?,
deviceFamily: String? = nil,
modelIdentifier: String? = nil,
remoteAddress: String? = nil)
{
self.type = type
@@ -112,6 +122,8 @@ public struct BridgePairRequest: Codable, Sendable {
self.displayName = displayName
self.platform = platform
self.version = version
self.deviceFamily = deviceFamily
self.modelIdentifier = modelIdentifier
self.remoteAddress = remoteAddress
}
}