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 { completion(nil) return } connection.receive(minimumIncompleteLength: 1, maximumLength: 1024*1024) { data, _, isComplete, error in if let data = data, !data.isEmpty { let image = UIImage(data: data) completion(image) } else { completion(nil) } } } func startVideoStream(onFrame: @escaping (UIImage?) -> Void) { guard let connection = connection, isConnected else { return } let command = LauncherCommand.startVideoStream() sendCommand(command) // Continuously receive video frames func receiveFrame() { connection.receive(minimumIncompleteLength: 1, maximumLength: 1024*1024) { data, _, isComplete, error in if let data = data, !data.isEmpty { let image = UIImage(data: data) DispatchQueue.main.async { onFrame(image) } // Continue receiving frames receiveFrame() } } } receiveFrame() } func stopVideoStream() { 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") } }