MenuBarExtra lists Bonjour services. Clicking on a service opens a WebSocket connection.
TODO: Consume the API data from the websocket queries and display them in the UI
This commit is contained in:
		@@ -19,6 +19,7 @@
 | 
			
		||||
		E180B61D2992D53700425DB0 /* PrinterObjectsQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B61C2992D53700425DB0 /* PrinterObjectsQuery.swift */; };
 | 
			
		||||
		E180B61F2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B61E2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift */; };
 | 
			
		||||
		E180B6222993256E00425DB0 /* PrinterRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B6212993256E00425DB0 /* PrinterRequestManager.swift */; };
 | 
			
		||||
		E1E8B07729949E2700BABE4B /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = E1E8B07629949E2700BABE4B /* Starscream */; };
 | 
			
		||||
/* End PBXBuildFile section */
 | 
			
		||||
 | 
			
		||||
/* Begin PBXContainerItemProxy section */
 | 
			
		||||
@@ -63,6 +64,7 @@
 | 
			
		||||
			isa = PBXFrameworksBuildPhase;
 | 
			
		||||
			buildActionMask = 2147483647;
 | 
			
		||||
			files = (
 | 
			
		||||
				E1E8B07729949E2700BABE4B /* Starscream in Frameworks */,
 | 
			
		||||
			);
 | 
			
		||||
			runOnlyForDeploymentPostprocessing = 0;
 | 
			
		||||
		};
 | 
			
		||||
@@ -162,6 +164,9 @@
 | 
			
		||||
			dependencies = (
 | 
			
		||||
			);
 | 
			
		||||
			name = KlipperMon;
 | 
			
		||||
			packageProductDependencies = (
 | 
			
		||||
				E1E8B07629949E2700BABE4B /* Starscream */,
 | 
			
		||||
			);
 | 
			
		||||
			productName = KlipperMon;
 | 
			
		||||
			productReference = E180B5E52992CD9100425DB0 /* KlipperMon.app */;
 | 
			
		||||
			productType = "com.apple.product-type.application";
 | 
			
		||||
@@ -234,6 +239,9 @@
 | 
			
		||||
				Base,
 | 
			
		||||
			);
 | 
			
		||||
			mainGroup = E180B5DC2992CD9100425DB0;
 | 
			
		||||
			packageReferences = (
 | 
			
		||||
				E1E8B07529949E2700BABE4B /* XCRemoteSwiftPackageReference "Starscream" */,
 | 
			
		||||
			);
 | 
			
		||||
			productRefGroup = E180B5E62992CD9100425DB0 /* Products */;
 | 
			
		||||
			projectDirPath = "";
 | 
			
		||||
			projectRoot = "";
 | 
			
		||||
@@ -601,6 +609,25 @@
 | 
			
		||||
		};
 | 
			
		||||
/* End XCConfigurationList section */
 | 
			
		||||
 | 
			
		||||
/* Begin XCRemoteSwiftPackageReference section */
 | 
			
		||||
		E1E8B07529949E2700BABE4B /* XCRemoteSwiftPackageReference "Starscream" */ = {
 | 
			
		||||
			isa = XCRemoteSwiftPackageReference;
 | 
			
		||||
			repositoryURL = "https://github.com/daltoniam/Starscream.git";
 | 
			
		||||
			requirement = {
 | 
			
		||||
				kind = upToNextMajorVersion;
 | 
			
		||||
				minimumVersion = 4.0.0;
 | 
			
		||||
			};
 | 
			
		||||
		};
 | 
			
		||||
/* End XCRemoteSwiftPackageReference section */
 | 
			
		||||
 | 
			
		||||
/* Begin XCSwiftPackageProductDependency section */
 | 
			
		||||
		E1E8B07629949E2700BABE4B /* Starscream */ = {
 | 
			
		||||
			isa = XCSwiftPackageProductDependency;
 | 
			
		||||
			package = E1E8B07529949E2700BABE4B /* XCRemoteSwiftPackageReference "Starscream" */;
 | 
			
		||||
			productName = Starscream;
 | 
			
		||||
		};
 | 
			
		||||
/* End XCSwiftPackageProductDependency section */
 | 
			
		||||
 | 
			
		||||
/* Begin XCVersionGroup section */
 | 
			
		||||
		E180B5F32992CD9200425DB0 /* KlipperMon.xcdatamodeld */ = {
 | 
			
		||||
			isa = XCVersionGroup;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>NSBonjourServices</key>
 | 
			
		||||
	<array>
 | 
			
		||||
		<string>_moonraker._tcp.</string>
 | 
			
		||||
		<string>_http._tcp.</string>
 | 
			
		||||
	</array>
 | 
			
		||||
	<key>CFBundleURLTypes</key>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import AppKit
 | 
			
		||||
import Network
 | 
			
		||||
 | 
			
		||||
struct KlipperMenuBarButtonStyle: ButtonStyle {
 | 
			
		||||
    func makeBody(configuration: Configuration) -> some View {
 | 
			
		||||
@@ -36,9 +35,9 @@ struct KlipperMonMenuBarExtraView: View {
 | 
			
		||||
    @State var hotendHotTemp: Bool = false
 | 
			
		||||
    @State var bedHotTemp: Bool = false
 | 
			
		||||
    
 | 
			
		||||
    @State var nwBrowserDiscoveredItems: [NWEndpoint] = []
 | 
			
		||||
    //@State var nwBrowserDiscoveredItems: [NWEndpoint] = []
 | 
			
		||||
    
 | 
			
		||||
    var nwBrowser = NWBrowser(for: .bonjour(type: "_moonraker._tcp.", domain: "local."), using: .tcp)
 | 
			
		||||
    //var nwBrowser = NWBrowser(for: .bonjour(type: "_moonraker._tcp.", domain: "local."), using: .tcp)
 | 
			
		||||
    
 | 
			
		||||
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
 | 
			
		||||
    
 | 
			
		||||
@@ -120,7 +119,7 @@ struct KlipperMonMenuBarExtraView: View {
 | 
			
		||||
        .frame(minWidth: 220, maxWidth: 250)
 | 
			
		||||
        .onReceive(timer) { input in
 | 
			
		||||
            Task {
 | 
			
		||||
                await printerManager.queryPrinterStats()
 | 
			
		||||
                //await printerManager.queryPrinterStats()
 | 
			
		||||
                
 | 
			
		||||
                if let query = printerManager.printerObjectsQuery {
 | 
			
		||||
                    hotendHotTemp = (query.result.status.extruder.temperature > DANGERTEMP) ? true : false
 | 
			
		||||
@@ -132,45 +131,11 @@ struct KlipperMonMenuBarExtraView: View {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // Testing bonjour stuff
 | 
			
		||||
        .onAppear {
 | 
			
		||||
            nwBrowser.browseResultsChangedHandler = { (newResults, changes) in
 | 
			
		||||
                print("[update] Results changed.")
 | 
			
		||||
                newResults.forEach { result in
 | 
			
		||||
                    print(result)
 | 
			
		||||
                    self.nwBrowserDiscoveredItems.append(result.endpoint)
 | 
			
		||||
                }
 | 
			
		||||
                //self.nwBrowserDiscoveredItems.append(newResults.description)
 | 
			
		||||
            }
 | 
			
		||||
            nwBrowser.stateUpdateHandler = { 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.start(queue: DispatchQueue.main)
 | 
			
		||||
        }
 | 
			
		||||
        ForEach(nwBrowserDiscoveredItems, id: \.hashValue) { item in
 | 
			
		||||
        ForEach(printerManager.nwBrowserDiscoveredItems, id: \.hashValue) { endpoint in
 | 
			
		||||
            Button {
 | 
			
		||||
                let connection = NWConnection(to: item, using: .tcp)
 | 
			
		||||
                connection.stateUpdateHandler = { newState in
 | 
			
		||||
                    switch newState {
 | 
			
		||||
                    case .failed(let error):
 | 
			
		||||
                        print("[error] nwconnection: \(error)")
 | 
			
		||||
                    case .ready:
 | 
			
		||||
                        print("[ready] nwconnection")
 | 
			
		||||
                    default:
 | 
			
		||||
                        break
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                connection.start(queue: DispatchQueue.main)
 | 
			
		||||
                printerManager.resolveBonjourHost(endpoint)
 | 
			
		||||
            } label: {
 | 
			
		||||
                Text(item.debugDescription)
 | 
			
		||||
                Text(endpoint.debugDescription)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,21 +7,148 @@
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
import Network
 | 
			
		||||
import Starscream
 | 
			
		||||
 | 
			
		||||
@MainActor
 | 
			
		||||
class PrinterRequestManager: ObservableObject {
 | 
			
		||||
struct JsonRpcRequest: Encodable {
 | 
			
		||||
    let jsonrpc = "2.0"
 | 
			
		||||
    let method: String
 | 
			
		||||
    let params: [String: String]
 | 
			
		||||
    //let id = UUID()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
    func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocket) {
 | 
			
		||||
        switch event {
 | 
			
		||||
        case .connected(let headers):
 | 
			
		||||
            //isConnected = true
 | 
			
		||||
            print("websocket is connected: \(headers)")
 | 
			
		||||
        case .disconnected(let reason, let code):
 | 
			
		||||
            //isConnected = false
 | 
			
		||||
            print("websocket is disconnected: \(reason) with code: \(code)")
 | 
			
		||||
        case .text(let string):
 | 
			
		||||
            print("Received text: \(string)")
 | 
			
		||||
        case .binary(let data):
 | 
			
		||||
            print("Received data: \(data.count)")
 | 
			
		||||
        case .ping(_):
 | 
			
		||||
            break
 | 
			
		||||
        case .pong(_):
 | 
			
		||||
            break
 | 
			
		||||
        case .viabilityChanged(_):
 | 
			
		||||
            break
 | 
			
		||||
        case .reconnectSuggested(_):
 | 
			
		||||
            break
 | 
			
		||||
        case .cancelled:
 | 
			
		||||
            //isConnected = false
 | 
			
		||||
            break
 | 
			
		||||
        case .error(let error):
 | 
			
		||||
            print("[error] Starscream: \(error.debugDescription)")
 | 
			
		||||
            //isConnected = false
 | 
			
		||||
            //handleError(error)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // REST query results
 | 
			
		||||
    @Published var printerObjectsQuery: PrinterObjectsQuery?
 | 
			
		||||
    
 | 
			
		||||
    // Websocket RPC-JSON endpoints discovered via bonjour
 | 
			
		||||
    @Published var nwBrowserDiscoveredItems: [NWEndpoint] = []
 | 
			
		||||
    
 | 
			
		||||
    @Published var printerCommsOkay = false
 | 
			
		||||
    
 | 
			
		||||
    var socket: WebSocket!
 | 
			
		||||
    
 | 
			
		||||
    private var socketHost, socketPort: String?
 | 
			
		||||
    
 | 
			
		||||
    //var nwBrowser: NWBrowser!
 | 
			
		||||
    let nwBrowser = NWBrowser(for: .bonjour(type: "_moonraker._tcp", domain: "local."), using: .tcp)
 | 
			
		||||
    var connection: NWConnection!
 | 
			
		||||
    
 | 
			
		||||
    static let shared = PrinterRequestManager()
 | 
			
		||||
    
 | 
			
		||||
    //let nwBrowser = NWBrowser(for: .bonjour(type: "_moonraker._tcp", domain: "local."), using: .tcp)
 | 
			
		||||
    
 | 
			
		||||
    private init() {
 | 
			
		||||
        // MARK: Starscream shit
 | 
			
		||||
        //
 | 
			
		||||
        //
 | 
			
		||||
        print("init PRM..")
 | 
			
		||||
        //        var request = URLRequest(url: URL(string: "http://10.0.21.39:7125/websocket")!)
 | 
			
		||||
        //        request.timeoutInterval = 5
 | 
			
		||||
        //        socket = WebSocket(request: request)
 | 
			
		||||
        //        socket.delegate = self
 | 
			
		||||
        //socket.connect()
 | 
			
		||||
        
 | 
			
		||||
        //let data = try! JSONEncoder().encode(JsonRpcRequest(method: "printer.objects.list", params: [:]))
 | 
			
		||||
        //socket.write(data: data)
 | 
			
		||||
        
 | 
			
		||||
        // MARK: NWBrowser shit
 | 
			
		||||
        //
 | 
			
		||||
        //
 | 
			
		||||
        nwBrowser.browseResultsChangedHandler = { (newResults, changes) in
 | 
			
		||||
            print("[update] Results changed.")
 | 
			
		||||
            newResults.forEach { result in
 | 
			
		||||
                print(result)
 | 
			
		||||
                self.nwBrowserDiscoveredItems.append(result.endpoint)
 | 
			
		||||
            }
 | 
			
		||||
            //self.nwBrowserDiscoveredItems.append(newResults.description)
 | 
			
		||||
        }
 | 
			
		||||
        nwBrowser.stateUpdateHandler = { newState in
 | 
			
		||||
            switch newState {
 | 
			
		||||
            case .failed(let error):
 | 
			
		||||
                print("[error] nwbrowser: \(error)")
 | 
			
		||||
            case .ready:
 | 
			
		||||
                print("[ready] nwbrowser")
 | 
			
		||||
                if let innerEndpoint = self.connection?.currentPath?.remoteEndpoint, case .hostPort(let host, let port) = innerEndpoint {
 | 
			
		||||
                    print("Connected to:", "\(host):\(port)")
 | 
			
		||||
                }
 | 
			
		||||
            case .setup:
 | 
			
		||||
                print("[setup] nwbrowser")
 | 
			
		||||
            default:
 | 
			
		||||
                break
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        nwBrowser.start(queue: DispatchQueue.main)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private func openWebsocket() {
 | 
			
		||||
        if let host = socketHost, let port = socketPort {
 | 
			
		||||
            //let fullUrlString = "http://\(socketHost):\(socketPort)/websocket"
 | 
			
		||||
            var request = URLRequest(url: URL(string: "http://\(host):\(port)/websocket")!)
 | 
			
		||||
            request.timeoutInterval = 5
 | 
			
		||||
            socket = WebSocket(request: request)
 | 
			
		||||
            socket.delegate = self
 | 
			
		||||
            socket.connect()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func resolveBonjourHost(_ endpoint: NWEndpoint) {
 | 
			
		||||
        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 {
 | 
			
		||||
                    print("Connected to \(host):\(port)")
 | 
			
		||||
                    let hostString = "\(host)"
 | 
			
		||||
                    let regex = try! Regex("%en0")
 | 
			
		||||
                    let match = hostString.firstMatch(of: regex)
 | 
			
		||||
                    let sanitizedHost = hostString.replacingOccurrences(of: match!.0, with: "")
 | 
			
		||||
                    
 | 
			
		||||
                    print("[sanitized] Resolved \(sanitizedHost):\(port)")
 | 
			
		||||
                    socketHost = sanitizedHost
 | 
			
		||||
                    socketPort = "\(port)"
 | 
			
		||||
                    connection.cancel()
 | 
			
		||||
                    self.openWebsocket()
 | 
			
		||||
                }
 | 
			
		||||
            default:
 | 
			
		||||
                break
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        connection.start(queue: .global())
 | 
			
		||||
        //connection.cancel()
 | 
			
		||||
        
 | 
			
		||||
        //self.openWebsocket()
 | 
			
		||||
    }
 | 
			
		||||
    // NWConnection shit
 | 
			
		||||
    //connection = NWConnection(
 | 
			
		||||
    
 | 
			
		||||
    func queryPrinterStats() async {
 | 
			
		||||
        guard let url = URL(string: "http://10.0.21.39/printer/objects/query?extruder&virtual_sdcard&print_stats&heater_bed") else {
 | 
			
		||||
            fatalError("Missing URL")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user