Refactor code, ensure websocket connection (and @ObservableObject updates) occur on main thread

This commit is contained in:
Madeline 2023-03-20 20:31:18 -04:00
parent ba42ff1207
commit 0a8670ed71
3 changed files with 72 additions and 78 deletions

View File

@ -13,7 +13,7 @@ struct SoyuzApp: App {
@State var currentIcon = "move.3d" @State var currentIcon = "move.3d"
@ObservedObject var printerManager = PrinterRequestManager() @ObservedObject static var printerManager = PrinterRequestManager()
var body: some Scene { var body: some Scene {
// WindowGroup(id: "floating-stats") { // WindowGroup(id: "floating-stats") {
@ -22,13 +22,13 @@ struct SoyuzApp: App {
// } // }
WindowGroup("Configuration", id: "soyuz_cfg", content: { WindowGroup("Configuration", id: "soyuz_cfg", content: {
PrinterConfigView(printerManager: printerManager) PrinterConfigView(printerManager: SoyuzApp.printerManager)
//.frame(minWidth: 300, maxWidth: 600, minHeight: 60, maxHeight: 100) //.frame(minWidth: 300, maxWidth: 600, minHeight: 60, maxHeight: 100)
}) })
.windowResizability(.contentSize) .windowResizability(.contentSize)
MenuBarExtra("Soyuz", systemImage: currentIcon) { MenuBarExtra("Soyuz", systemImage: currentIcon) {
SoyuzMenuBarExtraView(printerManager: printerManager, currentMenuBarIcon: $currentIcon) SoyuzMenuBarExtraView(printerManager: SoyuzApp.printerManager, currentMenuBarIcon: $currentIcon)
.padding([.top, .leading, .trailing], 8) .padding([.top, .leading, .trailing], 8)
.padding([.bottom], 6) .padding([.bottom], 6)
} }

View File

@ -10,8 +10,9 @@ import Network
import AppKit import AppKit
import Starscream import Starscream
// MARK: Bonjour Protocol
// Protocol defining minimal API for network discovery
// MARK: Net Discovery Protocol
protocol NetworkDiscoveryEngine { protocol NetworkDiscoveryEngine {
func startScan(queue: DispatchQueue) func startScan(queue: DispatchQueue)
func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void)) func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void))
@ -32,8 +33,9 @@ extension NWBrowser: NetworkDiscoveryEngine {
} }
} }
// MARK: Starscream Protocol
// Properly formatted JSON-RPC Request for use with Starscream
// MARK: JSON-RPC Request Codable
struct JsonRpcRequest: Codable { struct JsonRpcRequest: Codable {
var jsonrpc = "2.0" var jsonrpc = "2.0"
let method: String let method: String
@ -49,15 +51,17 @@ struct JsonRpcRequest: Codable {
} }
} }
// MARK: PrinterRequestManager
//@MainActor //@MainActor
class PrinterRequestManager: ObservableObject, WebSocketDelegate { class PrinterRequestManager: ObservableObject, WebSocketDelegate {
let WEBSOCKET_TIMEOUT_INTERVAL: TimeInterval = 60.0 let WEBSOCKET_TIMEOUT_INTERVAL: TimeInterval = 60.0
// Debug stuff // Debug timestamp stuff
let startDate = Date() let startDate = Date()
let startDateString: String let startDateString: String
let filename: URL let filename: URL
// Debug file writing stuff
func writeToDebugLog(_ output: String) { func writeToDebugLog(_ output: String) {
do { do {
let fileHandle = try FileHandle(forWritingTo: filename) let fileHandle = try FileHandle(forWritingTo: filename)
@ -91,38 +95,12 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
@Published var socketPort: String @Published var socketPort: String
let nwBrowser: NetworkDiscoveryEngine let nwBrowser: NetworkDiscoveryEngine
//let nwBrowser = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)
var connection: NWConnection! var connection: NWConnection!
var socket: WebSocket? var socket: WebSocket?
var lastPingDate = Date() var lastPingDate = Date()
// Parse a JSON-RPC query-response message // MARK: PRM init()
func parse_response(_ response: jsonRpcResponse) {
state = response.result.status.print_stats?.state ?? ""
progress = response.result.status.virtual_sdcard?.progress ?? 0.0
extruderTemperature = response.result.status.extruder?.temperature ?? 0.0
bedTemperature = response.result.status.heater_bed?.temperature ?? 0.0
print(response)
}
// Parse a JSON-RPC update message
func parse_update(_ update: jsonRpcUpdate) {
if let newState = update.params.status?.print_stats?.state {
state = newState
}
if let newProgress = update.params.status?.virtual_sdcard?.progress {
progress = newProgress
}
if let newExtruderTemp = update.params.status?.extruder?.temperature {
extruderTemperature = newExtruderTemp
}
if let newBedTemp = update.params.status?.heater_bed?.temperature {
bedTemperature = newBedTemp
}
}
init(browser: NetworkDiscoveryEngine = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)) { init(browser: NetworkDiscoveryEngine = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)) {
state = "" state = ""
progress = 0.0 progress = 0.0
@ -131,9 +109,8 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
socketHost = "" socketHost = ""
socketPort = "" socketPort = ""
nwBrowser = browser nwBrowser = browser
//reconnectionTimer = nil
// MARK: Debug stuff // Debug output-to-file functionality
startDateString = "\(startDate)\n\n" startDateString = "\(startDate)\n\n"
filename = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("klippermon-debug-\(startDateString).txt") filename = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("klippermon-debug-\(startDateString).txt")
@ -143,7 +120,7 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
print("[error] Couldn't write to \(filename) - \(error)") print("[error] Couldn't write to \(filename) - \(error)")
} }
// MARK: Bonjour browser initialization at instantiation // Bonjour browser results changed handler
nwBrowser.setBrowseResultsChangedHandler({ (newResults, changes) in nwBrowser.setBrowseResultsChangedHandler({ (newResults, changes) in
print("[update] Results changed.") print("[update] Results changed.")
newResults.forEach { result in newResults.forEach { result in
@ -164,12 +141,8 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
break break
} }
}) })
// Start up the bonjour browser, get results and process them in the update handler
nwBrowser.startScan(queue: DispatchQueue.main)
// Screen sleep functionality // Set up sleep/wake notification observers
// NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(screenDidSleep(_:)), name: NSWorkspace.screensDidSleepNotification, object: nil)
// NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(screenDidWake(_:)), name: NSWorkspace.screensDidWakeNotification, object: nil)
let center = NSWorkspace.shared.notificationCenter; let center = NSWorkspace.shared.notificationCenter;
let mainQueue = OperationQueue.main let mainQueue = OperationQueue.main
@ -180,30 +153,15 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
center.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: mainQueue) { notification in center.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: mainQueue) { notification in
self.screenChangedSleepState(notification) self.screenChangedSleepState(notification)
} }
}
func screenChangedSleepState(_ notification: Notification) { // Start up the bonjour browser, get results and process them in the update handler
switch(notification.name) { nwBrowser.startScan(queue: DispatchQueue.main)
case NSWorkspace.screensDidSleepNotification:
socket?.disconnect()
case NSWorkspace.screensDidWakeNotification:
self.openWebsocket()
default:
return
}
}
func screenDidWake(_ notification: Notification) {
print("Screen woke: \(notification.name)")
if socket != nil {
self.openWebsocket()
}
} }
// Called from the UI with an endpoint. // Called from the UI with an endpoint.
// Momentarily connect/disconnects from the endpoint to retrieve the host/port // Momentarily connect/disconnects from the endpoint to retrieve the host/port
// calls private function openWebsocket to process the host/port // calls private function openWebsocket to process the host/port
func resolveBonjourHost(_ endpoint: NWEndpoint) { func connectToBonjourEndpoint(_ endpoint: NWEndpoint) {
// Debug stuff // Debug stuff
endpoint.txtRecord?.forEach({ (key: String, value: NWTXTRecord.Entry) in endpoint.txtRecord?.forEach({ (key: String, value: NWTXTRecord.Entry) in
print("\(key): \(value)") print("\(key): \(value)")
@ -226,11 +184,14 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
print("[sanitized] Resolved \(sanitizedHost):\(port)") print("[sanitized] Resolved \(sanitizedHost):\(port)")
socketHost = sanitizedHost
socketPort = "\(port)"
connection.cancel() connection.cancel()
DispatchQueue.main.async {
self.socketHost = sanitizedHost
self.socketPort = "\(port)"
self.openWebsocket() self.openWebsocket()
} }
}
default: default:
break break
} }
@ -238,18 +199,10 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
connection.start(queue: .global()) connection.start(queue: .global())
} }
func reconnectWebsocket() {
if socket == nil {
return
}
socket!.disconnect() // MARK: Private functions
self.openWebsocket()
//socket!.write(ping: "PING!".data(using: .utf8)!)
}
// Opens the websocket connection // Opens the websocket connection
// TODO: host and port should be function arguments probably maybe
private func openWebsocket() { private func openWebsocket() {
//let fullUrlString = "http://\(socketHost):\(socketPort)/websocket" //let fullUrlString = "http://\(socketHost):\(socketPort)/websocket"
var request = URLRequest(url: URL(string: "http://\(socketHost):\(socketPort)/websocket")!) var request = URLRequest(url: URL(string: "http://\(socketHost):\(socketPort)/websocket")!)
@ -257,14 +210,29 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
socket = WebSocket(request: request) socket = WebSocket(request: request)
socket!.delegate = self socket!.delegate = self
socket!.connect() socket!.connect()
}
// TODO: Check that this keeps the connection alive properly private func reconnectWebsocket() {
Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [self] timer in if socket == nil {
//self.checkWebsocketIsAlive() return
}
socket!.disconnect()
self.openWebsocket()
}
// MARK: Callsbacks
func screenChangedSleepState(_ notification: Notification) {
switch(notification.name) {
case NSWorkspace.screensDidSleepNotification:
socket?.disconnect()
case NSWorkspace.screensDidWakeNotification:
self.openWebsocket()
default:
return
} }
} }
// MARK: delegate callback for Starscream WebSocketClient
func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocket) { func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocket) {
switch event { switch event {
case .connected(let headers): case .connected(let headers):
@ -325,4 +293,30 @@ class PrinterRequestManager: ObservableObject, WebSocketDelegate {
} }
} }
// MARK: JSON-RPC Parsing
// Parse a JSON-RPC query-response message
func parse_response(_ response: jsonRpcResponse) {
state = response.result.status.print_stats?.state ?? ""
progress = response.result.status.virtual_sdcard?.progress ?? 0.0
extruderTemperature = response.result.status.extruder?.temperature ?? 0.0
bedTemperature = response.result.status.heater_bed?.temperature ?? 0.0
print(response)
}
// Parse a JSON-RPC update message
func parse_update(_ update: jsonRpcUpdate) {
if let newState = update.params.status?.print_stats?.state {
state = newState
}
if let newProgress = update.params.status?.virtual_sdcard?.progress {
progress = newProgress
}
if let newExtruderTemp = update.params.status?.extruder?.temperature {
extruderTemperature = newExtruderTemp
}
if let newBedTemp = update.params.status?.heater_bed?.temperature {
bedTemperature = newBedTemp
}
}
} }

View File

@ -33,7 +33,7 @@ struct PrinterConfigView: View {
HStack { HStack {
Text(result.endpoint.toFriendlyString()) Text(result.endpoint.toFriendlyString())
Button { Button {
printerManager.resolveBonjourHost(result.endpoint) printerManager.connectToBonjourEndpoint(result.endpoint)
} label: { } label: {
Text("Connect") Text("Connect")
.foregroundColor(.white) .foregroundColor(.white)