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:
Madeline 2023-02-09 00:29:15 -05:00
parent 5f429b8cb4
commit ee2583172a
4 changed files with 165 additions and 45 deletions

View File

@ -19,6 +19,7 @@
E180B61D2992D53700425DB0 /* PrinterObjectsQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B61C2992D53700425DB0 /* PrinterObjectsQuery.swift */; }; E180B61D2992D53700425DB0 /* PrinterObjectsQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B61C2992D53700425DB0 /* PrinterObjectsQuery.swift */; };
E180B61F2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B61E2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift */; }; E180B61F2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B61E2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift */; };
E180B6222993256E00425DB0 /* PrinterRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B6212993256E00425DB0 /* PrinterRequestManager.swift */; }; E180B6222993256E00425DB0 /* PrinterRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B6212993256E00425DB0 /* PrinterRequestManager.swift */; };
E1E8B07729949E2700BABE4B /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = E1E8B07629949E2700BABE4B /* Starscream */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -63,6 +64,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E1E8B07729949E2700BABE4B /* Starscream in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -162,6 +164,9 @@
dependencies = ( dependencies = (
); );
name = KlipperMon; name = KlipperMon;
packageProductDependencies = (
E1E8B07629949E2700BABE4B /* Starscream */,
);
productName = KlipperMon; productName = KlipperMon;
productReference = E180B5E52992CD9100425DB0 /* KlipperMon.app */; productReference = E180B5E52992CD9100425DB0 /* KlipperMon.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@ -234,6 +239,9 @@
Base, Base,
); );
mainGroup = E180B5DC2992CD9100425DB0; mainGroup = E180B5DC2992CD9100425DB0;
packageReferences = (
E1E8B07529949E2700BABE4B /* XCRemoteSwiftPackageReference "Starscream" */,
);
productRefGroup = E180B5E62992CD9100425DB0 /* Products */; productRefGroup = E180B5E62992CD9100425DB0 /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@ -601,6 +609,25 @@
}; };
/* End XCConfigurationList section */ /* 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 */ /* Begin XCVersionGroup section */
E180B5F32992CD9200425DB0 /* KlipperMon.xcdatamodeld */ = { E180B5F32992CD9200425DB0 /* KlipperMon.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;

View File

@ -4,6 +4,7 @@
<dict> <dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_moonraker._tcp.</string>
<string>_http._tcp.</string> <string>_http._tcp.</string>
</array> </array>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>

View File

@ -7,7 +7,6 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
import Network
struct KlipperMenuBarButtonStyle: ButtonStyle { struct KlipperMenuBarButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View { func makeBody(configuration: Configuration) -> some View {
@ -36,9 +35,9 @@ struct KlipperMonMenuBarExtraView: View {
@State var hotendHotTemp: Bool = false @State var hotendHotTemp: Bool = false
@State var bedHotTemp: 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() let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@ -120,7 +119,7 @@ struct KlipperMonMenuBarExtraView: View {
.frame(minWidth: 220, maxWidth: 250) .frame(minWidth: 220, maxWidth: 250)
.onReceive(timer) { input in .onReceive(timer) { input in
Task { Task {
await printerManager.queryPrinterStats() //await printerManager.queryPrinterStats()
if let query = printerManager.printerObjectsQuery { if let query = printerManager.printerObjectsQuery {
hotendHotTemp = (query.result.status.extruder.temperature > DANGERTEMP) ? true : false hotendHotTemp = (query.result.status.extruder.temperature > DANGERTEMP) ? true : false
@ -132,45 +131,11 @@ struct KlipperMonMenuBarExtraView: View {
} }
} }
// Testing bonjour stuff // Testing bonjour stuff
.onAppear { ForEach(printerManager.nwBrowserDiscoveredItems, id: \.hashValue) { endpoint in
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
Button { Button {
let connection = NWConnection(to: item, using: .tcp) printerManager.resolveBonjourHost(endpoint)
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)
} label: { } label: {
Text(item.debugDescription) Text(endpoint.debugDescription)
} }
} }
} }

View File

@ -7,20 +7,147 @@
import Foundation import Foundation
import Network import Network
import Starscream
@MainActor struct JsonRpcRequest: Encodable {
class PrinterRequestManager: ObservableObject { 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? @Published var printerObjectsQuery: PrinterObjectsQuery?
// Websocket RPC-JSON endpoints discovered via bonjour
@Published var nwBrowserDiscoveredItems: [NWEndpoint] = []
@Published var printerCommsOkay = false @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() static let shared = PrinterRequestManager()
//let nwBrowser = NWBrowser(for: .bonjour(type: "_moonraker._tcp", domain: "local."), using: .tcp)
private init() { 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 { func queryPrinterStats() async {
guard let url = URL(string: "http://10.0.21.39/printer/objects/query?extruder&virtual_sdcard&print_stats&heater_bed") else { guard let url = URL(string: "http://10.0.21.39/printer/objects/query?extruder&virtual_sdcard&print_stats&heater_bed") else {