diff --git a/KlipperMon.xcodeproj/project.pbxproj b/KlipperMon.xcodeproj/project.pbxproj index baa7bab..10732d2 100644 --- a/KlipperMon.xcodeproj/project.pbxproj +++ b/KlipperMon.xcodeproj/project.pbxproj @@ -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; diff --git a/KlipperMon/Info.plist b/KlipperMon/Info.plist index 50e6c17..2f09cb0 100644 --- a/KlipperMon/Info.plist +++ b/KlipperMon/Info.plist @@ -4,6 +4,7 @@ NSBonjourServices + _moonraker._tcp. _http._tcp. CFBundleURLTypes diff --git a/KlipperMon/KlipperMonMenuBarExtraView.swift b/KlipperMon/KlipperMonMenuBarExtraView.swift index 0ffbfa8..ceeb914 100644 --- a/KlipperMon/KlipperMonMenuBarExtraView.swift +++ b/KlipperMon/KlipperMonMenuBarExtraView.swift @@ -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) } } } diff --git a/KlipperMon/PrinterRequestManager.swift b/KlipperMon/PrinterRequestManager.swift index c02d57d..d2d1f14 100644 --- a/KlipperMon/PrinterRequestManager.swift +++ b/KlipperMon/PrinterRequestManager.swift @@ -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")