From 3863fe64124502ec0f356f55c01c9f5eba734ee8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Dec 2025 12:28:34 +0000 Subject: [PATCH] fix(ios): stabilize voice wake + bridge UI --- apps/ios/README.md | 2 +- .../Sources/Bridge/BridgeDiscoveryModel.swift | 10 ++++---- .../ios/Sources/Bridge/BridgeEndpointID.swift | 19 +++++++++++++++ apps/ios/Sources/Info.plist | 8 +++---- apps/ios/Sources/Model/NodeAppModel.swift | 10 ++++---- apps/ios/Sources/Settings/SettingsTab.swift | 8 ++----- apps/ios/Sources/Voice/VoiceWakeManager.swift | 23 ++++++++++++------- apps/ios/project.yml | 8 +++---- docs/gateway/pairing.md | 6 +++++ 9 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 apps/ios/Sources/Bridge/BridgeEndpointID.swift diff --git a/apps/ios/README.md b/apps/ios/README.md index cc7f5cba2..2d0dd056f 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -1,4 +1,4 @@ -# ClawdisNode (iOS) +# ClawdisKit (iOS) Internal-only SwiftUI app scaffold. diff --git a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift b/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift index 79f5b6d67..cf47b88a4 100644 --- a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift +++ b/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift @@ -5,9 +5,10 @@ import Network @MainActor final class BridgeDiscoveryModel: ObservableObject { struct DiscoveredBridge: Identifiable, Equatable { - var id: String { self.debugID } + var id: String { self.stableID } var name: String var endpoint: NWEndpoint + var stableID: String var debugID: String } @@ -54,7 +55,8 @@ final class BridgeDiscoveryModel: ObservableObject { return DiscoveredBridge( name: decodedName, endpoint: result.endpoint, - debugID: Self.prettyEndpointDebugID(result.endpoint)) + stableID: BridgeEndpointID.stableID(result.endpoint), + debugID: BridgeEndpointID.prettyDescription(result.endpoint)) default: return nil } @@ -73,8 +75,4 @@ final class BridgeDiscoveryModel: ObservableObject { self.bridges = [] self.statusText = "Stopped" } - - private static func prettyEndpointDebugID(_ endpoint: NWEndpoint) -> String { - BonjourEscapes.decode(String(describing: endpoint)) - } } diff --git a/apps/ios/Sources/Bridge/BridgeEndpointID.swift b/apps/ios/Sources/Bridge/BridgeEndpointID.swift new file mode 100644 index 000000000..e5662f433 --- /dev/null +++ b/apps/ios/Sources/Bridge/BridgeEndpointID.swift @@ -0,0 +1,19 @@ +import ClawdisKit +import Foundation +import Network + +enum BridgeEndpointID { + static func stableID(_ endpoint: NWEndpoint) -> String { + switch endpoint { + case let .service(name, type, domain, _): + // Keep this stable across encode/decode differences; use raw service tuple. + "\(type)|\(domain)|\(name)" + default: + String(describing: endpoint) + } + } + + static func prettyDescription(_ endpoint: NWEndpoint) -> String { + BonjourEscapes.decode(String(describing: endpoint)) + } +} diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index e525cf7b4..05d3889f5 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Clawdis Node + ClawdisKit CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -25,11 +25,11 @@ _clawdis-bridge._tcp NSLocalNetworkUsageDescription - Clawdis Node discovers and connects to your Clawdis bridge on the local network. + ClawdisKit discovers and connects to your Clawdis bridge on the local network. NSMicrophoneUsageDescription - Clawdis Node needs microphone access for voice wake. + ClawdisKit needs microphone access for voice wake. NSSpeechRecognitionUsageDescription - Clawdis Node uses on-device speech recognition for voice wake. + ClawdisKit uses on-device speech recognition for voice wake. UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 0c5227f2c..e86f1c677 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -9,7 +9,7 @@ final class NodeAppModel: ObservableObject { @Published var bridgeStatusText: String = "Not connected" @Published var bridgeServerName: String? @Published var bridgeRemoteAddress: String? - @Published var connectedBridgeDebugID: String? + @Published var connectedBridgeID: String? private let bridge = BridgeSession() private var bridgeTask: Task? @@ -58,7 +58,7 @@ final class NodeAppModel: ObservableObject { self.bridgeStatusText = "Connecting…" self.bridgeServerName = nil self.bridgeRemoteAddress = nil - self.connectedBridgeDebugID = BonjourEscapes.decode(String(describing: endpoint)) + self.connectedBridgeID = BridgeEndpointID.stableID(endpoint) self.bridgeTask = Task { do { @@ -96,14 +96,14 @@ final class NodeAppModel: ObservableObject { self.bridgeStatusText = "Disconnected" self.bridgeServerName = nil self.bridgeRemoteAddress = nil - self.connectedBridgeDebugID = nil + self.connectedBridgeID = nil } } catch { await MainActor.run { self.bridgeStatusText = "Bridge error: \(error.localizedDescription)" self.bridgeServerName = nil self.bridgeRemoteAddress = nil - self.connectedBridgeDebugID = nil + self.connectedBridgeID = nil } } } @@ -116,7 +116,7 @@ final class NodeAppModel: ObservableObject { self.bridgeStatusText = "Disconnected" self.bridgeServerName = nil self.bridgeRemoteAddress = nil - self.connectedBridgeDebugID = nil + self.connectedBridgeID = nil } func sendVoiceTranscript(text: String, sessionKey: String?) async throws { diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index 3b19b2332..1a07f731c 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -98,9 +98,9 @@ struct SettingsTab: View { Text("No bridges found yet.") .foregroundStyle(.secondary) } else { - let connectedID = self.appModel.connectedBridgeDebugID + let connectedID = self.appModel.connectedBridgeID let rows = self.discovery.bridges.filter { bridge in - let isConnected = bridge.debugID == connectedID + let isConnected = bridge.stableID == connectedID switch showing { case .all: return true @@ -117,10 +117,6 @@ struct SettingsTab: View { HStack { VStack(alignment: .leading, spacing: 2) { Text(bridge.name) - Text(bridge.debugID) - .font(.caption2) - .foregroundStyle(.secondary) - .lineLimit(1) } Spacer() diff --git a/apps/ios/Sources/Voice/VoiceWakeManager.swift b/apps/ios/Sources/Voice/VoiceWakeManager.swift index aacb5bba6..91871ca22 100644 --- a/apps/ios/Sources/Voice/VoiceWakeManager.swift +++ b/apps/ios/Sources/Voice/VoiceWakeManager.swift @@ -2,6 +2,13 @@ import AVFAudio import Foundation import Speech +private func makeAudioTapEnqueueCallback(queue: AudioBufferQueue) -> AVAudioNodeTapBlock { + { buffer, _ in + // This callback is invoked on a realtime audio thread/queue. Keep it tiny and nonisolated. + queue.enqueueCopy(of: buffer) + } +} + private final class AudioBufferQueue: @unchecked Sendable { private let lock = NSLock() private var buffers: [AVAudioPCMBuffer] = [] @@ -41,7 +48,7 @@ extension AVAudioPCMBuffer { let channels = Int(format.channelCount) let frames = Int(frameLength) for ch in 0..