Separate bonjour concerns in PrinterRequestManager into BonjourBrowser, add unit test for BonjourBrowser
This commit is contained in:
		@@ -17,6 +17,7 @@
 | 
			
		||||
	</array>
 | 
			
		||||
	<key>NSBonjourServices</key>
 | 
			
		||||
	<array>
 | 
			
		||||
		<string>_xctest._tcp.</string>
 | 
			
		||||
		<string>_moonraker._tcp.</string>
 | 
			
		||||
		<string>_http._tcp.</string>
 | 
			
		||||
	</array>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,5 +8,7 @@
 | 
			
		||||
	<true/>
 | 
			
		||||
	<key>com.apple.security.network.client</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
	<key>com.apple.security.network.server</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								Soyuz/ViewModels/BonjourBrowser.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								Soyuz/ViewModels/BonjourBrowser.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
//
 | 
			
		||||
//  BonjourBrowser.swift
 | 
			
		||||
//  Soyuz
 | 
			
		||||
//
 | 
			
		||||
//  Created by maddiefuzz on 3/20/23.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
import Network
 | 
			
		||||
 | 
			
		||||
// Protocol defining minimal API for network discovery
 | 
			
		||||
// MARK: Net Discovery Protocol
 | 
			
		||||
protocol NetworkDiscoveryEngine {
 | 
			
		||||
    func startScan(queue: DispatchQueue)
 | 
			
		||||
    
 | 
			
		||||
    func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void))
 | 
			
		||||
    func setStateUpdateHandler(_ handler: @escaping ((NWBrowser.State) -> Void))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension NWBrowser: NetworkDiscoveryEngine {
 | 
			
		||||
    
 | 
			
		||||
    func startScan(queue: DispatchQueue) {
 | 
			
		||||
        start(queue: queue)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void)) {
 | 
			
		||||
        self.browseResultsChangedHandler = handler
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func setStateUpdateHandler(_ handler: @escaping ((State) -> Void)) {
 | 
			
		||||
        self.stateUpdateHandler = handler
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: BonjourBrowser
 | 
			
		||||
 | 
			
		||||
class BonjourBrowser: ObservableObject {
 | 
			
		||||
    @Published var NDEngineResults: [NWBrowser.Result] = []
 | 
			
		||||
    
 | 
			
		||||
    private let nwBrowser: NetworkDiscoveryEngine
 | 
			
		||||
    var connection: NWConnection!
 | 
			
		||||
    
 | 
			
		||||
    // TEMPORARY
 | 
			
		||||
//    var bonjourListener: NWListener?
 | 
			
		||||
 | 
			
		||||
    init(browser: NetworkDiscoveryEngine = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)) {
 | 
			
		||||
        nwBrowser = browser
 | 
			
		||||
        // Bonjour browser results changed handler
 | 
			
		||||
        nwBrowser.setBrowseResultsChangedHandler({ (newResults, changes) in
 | 
			
		||||
            print("[update] Results changed.")
 | 
			
		||||
            self.NDEngineResults.removeAll()
 | 
			
		||||
            newResults.forEach { result in
 | 
			
		||||
                print(result)
 | 
			
		||||
                self.NDEngineResults.append(result)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        // Bonjour browser state update handler
 | 
			
		||||
        nwBrowser.setStateUpdateHandler({ newState in
 | 
			
		||||
            switch newState {
 | 
			
		||||
            case .failed(let error):
 | 
			
		||||
                print("[error] nwbrowser: \(error)")
 | 
			
		||||
            case .ready:
 | 
			
		||||
                print("[ready] nwbrowser")
 | 
			
		||||
            case .setup:
 | 
			
		||||
                print("[setup] nwbrowser")
 | 
			
		||||
            default:
 | 
			
		||||
                break
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        nwBrowser.startScan(queue: DispatchQueue.main)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
@@ -11,78 +11,11 @@ import AppKit
 | 
			
		||||
import Starscream
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Protocol defining minimal API for network discovery
 | 
			
		||||
// MARK: Net Discovery Protocol
 | 
			
		||||
protocol NetworkDiscoveryEngine {
 | 
			
		||||
    func startScan(queue: DispatchQueue)
 | 
			
		||||
    func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void))
 | 
			
		||||
    func setStateUpdateHandler(_ handler: @escaping ((NWBrowser.State) -> Void))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension NWBrowser: NetworkDiscoveryEngine {
 | 
			
		||||
    func startScan(queue: DispatchQueue) {
 | 
			
		||||
        start(queue: queue)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void)) {
 | 
			
		||||
        self.browseResultsChangedHandler = handler
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func setStateUpdateHandler(_ handler: @escaping ((State) -> Void)) {
 | 
			
		||||
        self.stateUpdateHandler = handler
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Properly formatted JSON-RPC Request for use with Starscream
 | 
			
		||||
// MARK: JSON-RPC Request Codable
 | 
			
		||||
struct JsonRpcRequest: Codable {
 | 
			
		||||
    var jsonrpc = "2.0"
 | 
			
		||||
    let method: String
 | 
			
		||||
    let params: [String: [String: String?]]
 | 
			
		||||
    var id = 1
 | 
			
		||||
    
 | 
			
		||||
    func encode(to encoder: Encoder) throws {
 | 
			
		||||
        var container = encoder.container(keyedBy: CodingKeys.self)
 | 
			
		||||
        try container.encode(jsonrpc, forKey: .jsonrpc)
 | 
			
		||||
        try container.encode(method, forKey: .method)
 | 
			
		||||
        try container.encode(params, forKey: .params)
 | 
			
		||||
        try container.encode(id, forKey: .id)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: PrinterRequestManager
 | 
			
		||||
//@MainActor
 | 
			
		||||
class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
    let WEBSOCKET_TIMEOUT_INTERVAL: TimeInterval = 60.0
 | 
			
		||||
    
 | 
			
		||||
    // Debug timestamp stuff
 | 
			
		||||
    let startDate = Date()
 | 
			
		||||
    let startDateString: String
 | 
			
		||||
    let filename: URL
 | 
			
		||||
    
 | 
			
		||||
    // Debug file writing stuff
 | 
			
		||||
    func writeToDebugLog(_ output: String) {
 | 
			
		||||
        do {
 | 
			
		||||
            let fileHandle = try FileHandle(forWritingTo: filename)
 | 
			
		||||
            defer {
 | 
			
		||||
                fileHandle.closeFile()
 | 
			
		||||
            }
 | 
			
		||||
            fileHandle.seekToEndOfFile()
 | 
			
		||||
            let dateFormatter = DateFormatter()
 | 
			
		||||
            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SS"
 | 
			
		||||
            
 | 
			
		||||
            let debugOutput = String("\(dateFormatter.string(from: Date())) - \(output)\n")
 | 
			
		||||
            fileHandle.write(debugOutput.data(using: .utf8)!)
 | 
			
		||||
        } catch {
 | 
			
		||||
            print("[error] writeToDebugLog - \(error)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Websocket JSON-RPC endpoints discovered via bonjour
 | 
			
		||||
    @Published var nwBrowserDiscoveredItems: [NWBrowser.Result] = []
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    // Websocket JSON-RPC published data
 | 
			
		||||
    @Published var state: String
 | 
			
		||||
    @Published var progress: Double
 | 
			
		||||
@@ -94,53 +27,21 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
    @Published var socketHost: String
 | 
			
		||||
    @Published var socketPort: String
 | 
			
		||||
    
 | 
			
		||||
    let nwBrowser: NetworkDiscoveryEngine
 | 
			
		||||
    var connection: NWConnection!
 | 
			
		||||
    // Published NWConnection for listing connection information
 | 
			
		||||
    @Published var connection: NWConnection?
 | 
			
		||||
    
 | 
			
		||||
    private var socket: WebSocket?
 | 
			
		||||
    private var lastPingDate = Date()
 | 
			
		||||
    
 | 
			
		||||
    var socket: WebSocket?
 | 
			
		||||
    var lastPingDate = Date()
 | 
			
		||||
    
 | 
			
		||||
    // MARK: PRM init()
 | 
			
		||||
    init(browser: NetworkDiscoveryEngine = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)) {
 | 
			
		||||
    init() {
 | 
			
		||||
        state = ""
 | 
			
		||||
        progress = 0.0
 | 
			
		||||
        extruderTemperature = 0.0
 | 
			
		||||
        bedTemperature = 0.0
 | 
			
		||||
        socketHost = ""
 | 
			
		||||
        socketPort = ""
 | 
			
		||||
        nwBrowser = browser
 | 
			
		||||
        
 | 
			
		||||
        // Debug output-to-file functionality
 | 
			
		||||
        startDateString = "\(startDate)\n\n"
 | 
			
		||||
        filename = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("klippermon-debug-\(startDateString).txt")
 | 
			
		||||
        
 | 
			
		||||
        do {
 | 
			
		||||
            try startDateString.write(to: filename, atomically: true, encoding: .utf8)
 | 
			
		||||
        } catch {
 | 
			
		||||
            print("[error] Couldn't write to \(filename) - \(error)")
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Bonjour browser results changed handler
 | 
			
		||||
        nwBrowser.setBrowseResultsChangedHandler({ (newResults, changes) in
 | 
			
		||||
            print("[update] Results changed.")
 | 
			
		||||
            newResults.forEach { result in
 | 
			
		||||
                print(result)
 | 
			
		||||
                self.nwBrowserDiscoveredItems.append(result)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        // Bonjour browser state update handler
 | 
			
		||||
        nwBrowser.setStateUpdateHandler({ newState in
 | 
			
		||||
            switch newState {
 | 
			
		||||
            case .failed(let error):
 | 
			
		||||
                print("[error] nwbrowser: \(error)")
 | 
			
		||||
            case .ready:
 | 
			
		||||
                print("[ready] nwbrowser")
 | 
			
		||||
            case .setup:
 | 
			
		||||
                print("[setup] nwbrowser")
 | 
			
		||||
            default:
 | 
			
		||||
                break
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        // Set up sleep/wake notification observers
 | 
			
		||||
        let center = NSWorkspace.shared.notificationCenter;
 | 
			
		||||
@@ -153,9 +54,6 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
        center.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: mainQueue) { notification in
 | 
			
		||||
            self.screenChangedSleepState(notification)
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Start up the bonjour browser, get results and process them in the update handler
 | 
			
		||||
        nwBrowser.startScan(queue: DispatchQueue.main)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Called from the UI with an endpoint.
 | 
			
		||||
@@ -167,15 +65,17 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
            print("\(key): \(value)")
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        connection = NWConnection(to: endpoint, using: .tcp)
 | 
			
		||||
        connection.stateUpdateHandler = { [self] state in
 | 
			
		||||
        if connection == nil {
 | 
			
		||||
            connection = NWConnection(to: endpoint, using: .tcp)
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        connection?.stateUpdateHandler = { [self] state in
 | 
			
		||||
            switch state {
 | 
			
		||||
            case .ready:
 | 
			
		||||
                if let innerEndpoint = connection.currentPath?.remoteEndpoint, case .hostPort(let host, let port) = innerEndpoint {
 | 
			
		||||
                if let innerEndpoint = connection?.currentPath?.remoteEndpoint, case .hostPort(let host, let port) = innerEndpoint {
 | 
			
		||||
                    let hostPortDebugOutput = "Connected to \(host):\(port)"
 | 
			
		||||
                    
 | 
			
		||||
                    print(hostPortDebugOutput)
 | 
			
		||||
                    writeToDebugLog(hostPortDebugOutput)
 | 
			
		||||
                    
 | 
			
		||||
                    let hostString = "\(host)"
 | 
			
		||||
                    let regex = try! Regex("%(.+)")
 | 
			
		||||
@@ -184,7 +84,7 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
                    
 | 
			
		||||
                    print("[sanitized] Resolved \(sanitizedHost):\(port)")
 | 
			
		||||
                    
 | 
			
		||||
                    connection.cancel()
 | 
			
		||||
                    connection?.cancel()
 | 
			
		||||
                    
 | 
			
		||||
                    DispatchQueue.main.async {
 | 
			
		||||
                        self.socketHost = sanitizedHost
 | 
			
		||||
@@ -196,7 +96,11 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
                break
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        connection.start(queue: .global())
 | 
			
		||||
        connection?.start(queue: .global())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func disconnect() {
 | 
			
		||||
        socket?.disconnect()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
@@ -238,7 +142,6 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
        case .connected(let headers):
 | 
			
		||||
            isConnected = true
 | 
			
		||||
            print("websocket is connected: \(headers)")
 | 
			
		||||
            writeToDebugLog("Connected to WebSocket")
 | 
			
		||||
            let jsonRpcRequest = JsonRpcRequest(method: "printer.objects.subscribe",
 | 
			
		||||
                                                params: ["objects":
 | 
			
		||||
                                                            ["extruder": nil,
 | 
			
		||||
@@ -254,9 +157,7 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
        case .disconnected(let reason, let code):
 | 
			
		||||
            isConnected = false
 | 
			
		||||
            print("websocket is disconnected: \(reason) with code: \(code)")
 | 
			
		||||
            writeToDebugLog("Websocket is disconnected: \(reason) with code: \(code)")
 | 
			
		||||
        case .text(let string):
 | 
			
		||||
            self.writeToDebugLog(string)
 | 
			
		||||
            // Check for initial RPC response
 | 
			
		||||
            let statusResponse = try? JSONDecoder().decode(jsonRpcResponse.self, from: Data(string.utf8))
 | 
			
		||||
            if let statusResponseSafe = statusResponse {
 | 
			
		||||
@@ -267,7 +168,6 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
                self.parse_update(updateResponse)
 | 
			
		||||
            }
 | 
			
		||||
        case .binary(let data):
 | 
			
		||||
            self.writeToDebugLog(String(data: data, encoding: .utf8)!)
 | 
			
		||||
            print("Received data: \(data.count)")
 | 
			
		||||
        case .ping(_):
 | 
			
		||||
            print("PING! \(Date())")
 | 
			
		||||
@@ -320,3 +220,20 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Properly formatted JSON-RPC Request for use with Starscream
 | 
			
		||||
// MARK: JSON-RPC Request Codable
 | 
			
		||||
struct JsonRpcRequest: Codable {
 | 
			
		||||
    var jsonrpc = "2.0"
 | 
			
		||||
    let method: String
 | 
			
		||||
    let params: [String: [String: String?]]
 | 
			
		||||
    var id = 1
 | 
			
		||||
    
 | 
			
		||||
    func encode(to encoder: Encoder) throws {
 | 
			
		||||
        var container = encoder.container(keyedBy: CodingKeys.self)
 | 
			
		||||
        try container.encode(jsonrpc, forKey: .jsonrpc)
 | 
			
		||||
        try container.encode(method, forKey: .method)
 | 
			
		||||
        try container.encode(params, forKey: .params)
 | 
			
		||||
        try container.encode(id, forKey: .id)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,18 +8,20 @@
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import Network
 | 
			
		||||
 | 
			
		||||
// MARK: PrinterConfigView
 | 
			
		||||
struct PrinterConfigView: View {
 | 
			
		||||
    @ObservedObject var printerManager: PrinterRequestManager
 | 
			
		||||
    @ObservedObject var bonjourBrowser = BonjourBrowser()
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack {
 | 
			
		||||
            if(printerManager.isConnected) {
 | 
			
		||||
                HStack {
 | 
			
		||||
                    Image(systemName: "network")
 | 
			
		||||
                    Text(printerManager.connection.endpoint.toFriendlyString())
 | 
			
		||||
                    Text(printerManager.connection?.endpoint.toFriendlyString() ?? "Unknown Host")
 | 
			
		||||
                    Text("\(printerManager.socketHost):\(printerManager.socketPort)")
 | 
			
		||||
                    Button {
 | 
			
		||||
                        printerManager.socket?.disconnect()
 | 
			
		||||
                        printerManager.disconnect()
 | 
			
		||||
                    } label: {
 | 
			
		||||
                        Text("Disconnect")
 | 
			
		||||
                    }
 | 
			
		||||
@@ -29,7 +31,7 @@ struct PrinterConfigView: View {
 | 
			
		||||
                VStack {
 | 
			
		||||
                    Text("Auto-detected Printers")
 | 
			
		||||
                        .font(.title)
 | 
			
		||||
                    ForEach(printerManager.nwBrowserDiscoveredItems, id: \.hashValue) { result in
 | 
			
		||||
                    ForEach(bonjourBrowser.NDEngineResults , id: \.hashValue) { result in
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Text(result.endpoint.toFriendlyString())
 | 
			
		||||
                            Button {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user