import Foundation import Network import UIKit class NetworkService: ObservableObject { @Published var isConnected = false @Published var connectionStatus = "Disconnected" private var connection: NWConnection? private let queue = DispatchQueue(label: "NetworkService") // Configuration private let defaultHost = "192.168.1.100" // Change to your Pi's IP private let defaultPort: UInt16 = 8080 func connect(host: String = "", port: UInt16 = 0) { let targetHost = host.isEmpty ? defaultHost : host let targetPort = port == 0 ? defaultPort : port connection = NWConnection( host: NWEndpoint.Host(targetHost), port: NWEndpoint.Port(integerLiteral: targetPort), using: .tcp ) connection?.stateUpdateHandler = { [weak self] state in DispatchQueue.main.async { switch state { case .ready: self?.isConnected = true self?.connectionStatus = "Connected to \(targetHost):\(targetPort)" case .failed(let error): self?.isConnected = false self?.connectionStatus = "Failed: \(error.localizedDescription)" case .cancelled: self?.isConnected = false self?.connectionStatus = "Connection cancelled" default: self?.isConnected = false self?.connectionStatus = "Connecting..." } } } connection?.start(queue: queue) } func disconnect() { connection?.cancel() connection = nil isConnected = false connectionStatus = "Disconnected" } func sendCommand(_ command: LauncherCommand) { guard let connection = connection, isConnected else { print("No connection available") return } do { let data = try JSONEncoder().encode(command) connection.send(content: data, completion: .contentProcessed { error in if let error = error { print("Send error: \(error)") } }) } catch { print("Encoding error: \(error)") } } func requestPhoto(completion: @escaping (UIImage?) -> Void) { guard let connection = connection, isConnected else { DispatchQueue.main.async { completion(nil) } return } // Add timeout for photo requests let timeoutTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { _ in DispatchQueue.main.async { completion(nil) } } connection.receive(minimumIncompleteLength: 1, maximumLength: 1024*1024) { data, _, isComplete, error in timeoutTimer.invalidate() if let error = error { print("Photo receive error: \(error)") DispatchQueue.main.async { completion(nil) } return } if let data = data, !data.isEmpty { let image = UIImage(data: data) DispatchQueue.main.async { completion(image) } } else { DispatchQueue.main.async { completion(nil) } } } } private var isStreamingActive = false func startVideoStream(onFrame: @escaping (UIImage?) -> Void) { guard let connection = connection, isConnected else { return } let command = LauncherCommand.startVideoStream() sendCommand(command) isStreamingActive = true // Continuously receive video frames func receiveFrame() { guard isStreamingActive else { return } connection.receive(minimumIncompleteLength: 1, maximumLength: 1024*1024) { data, _, isComplete, error in guard self.isStreamingActive else { return } if let error = error { print("Video stream error: \(error)") DispatchQueue.main.async { onFrame(nil) } return } if let data = data, !data.isEmpty { let image = UIImage(data: data) DispatchQueue.main.async { onFrame(image) } // Continue receiving frames only if still streaming if self.isStreamingActive { receiveFrame() } } } } receiveFrame() } func stopVideoStream() { isStreamingActive = false let command = LauncherCommand.stopVideoStream() sendCommand(command) } } struct LauncherCommand: Codable { let action: String let angle: Double? let mode: String? let timestamp: Date init(action: String, angle: Double? = nil, mode: String? = nil) { self.action = action self.angle = angle self.mode = mode self.timestamp = Date() } } // Predefined commands extension LauncherCommand { static func aimLeft() -> LauncherCommand { LauncherCommand(action: "aim_left") } static func aimRight() -> LauncherCommand { LauncherCommand(action: "aim_right") } static func fire(angle: Double) -> LauncherCommand { LauncherCommand(action: "fire", angle: angle) } static func home() -> LauncherCommand { LauncherCommand(action: "home") } static func setMode(_ mode: String) -> LauncherCommand { LauncherCommand(action: "set_mode", mode: mode) } static func capturePhoto() -> LauncherCommand { LauncherCommand(action: "capture_photo") } static func startVideoStream() -> LauncherCommand { LauncherCommand(action: "start_video_stream") } static func stopVideoStream() -> LauncherCommand { LauncherCommand(action: "stop_video_stream") } static func stop() -> LauncherCommand { LauncherCommand(action: "stop") } static func status() -> LauncherCommand { LauncherCommand(action: "status") } }