Change PRM to MoonrakerSocketManager, project update
This commit is contained in:
239
Soyuz/ViewModels/MoonrakerSocketManager.swift
Executable file
239
Soyuz/ViewModels/MoonrakerSocketManager.swift
Executable file
@@ -0,0 +1,239 @@
|
||||
//
|
||||
// PrinterRequestManager.swift
|
||||
// KlipperMon
|
||||
//
|
||||
// Created by maddiefuzz on 2/7/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import AppKit
|
||||
import Starscream
|
||||
|
||||
|
||||
// MARK: PrinterRequestManager
|
||||
//@MainActor
|
||||
class MoonrakerSocketManager: ObservableObject, WebSocketDelegate {
|
||||
let WEBSOCKET_TIMEOUT_INTERVAL: TimeInterval = 60.0
|
||||
|
||||
// Websocket JSON-RPC published data
|
||||
@Published var state: String
|
||||
@Published var progress: Double
|
||||
@Published var extruderTemperature: Double
|
||||
@Published var bedTemperature: Double
|
||||
|
||||
// Active connection published data
|
||||
@Published var isConnected = false
|
||||
@Published var socketHost: String
|
||||
@Published var socketPort: String
|
||||
|
||||
// Published NWConnection for listing connection information
|
||||
@Published var connection: NWConnection?
|
||||
|
||||
private var socket: WebSocket?
|
||||
private var lastPingDate = Date()
|
||||
|
||||
|
||||
// MARK: PRM init()
|
||||
init() {
|
||||
state = ""
|
||||
progress = 0.0
|
||||
extruderTemperature = 0.0
|
||||
bedTemperature = 0.0
|
||||
socketHost = ""
|
||||
socketPort = ""
|
||||
|
||||
// Set up sleep/wake notification observers
|
||||
let center = NSWorkspace.shared.notificationCenter;
|
||||
let mainQueue = OperationQueue.main
|
||||
|
||||
center.addObserver(forName: NSWorkspace.screensDidWakeNotification, object: nil, queue: mainQueue) { notification in
|
||||
self.screenChangedSleepState(notification)
|
||||
}
|
||||
|
||||
center.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: mainQueue) { notification in
|
||||
self.screenChangedSleepState(notification)
|
||||
}
|
||||
}
|
||||
|
||||
// Called from the UI with an endpoint.
|
||||
// Momentarily connect/disconnects from the endpoint to retrieve the host/port
|
||||
// calls private function openWebsocket to process the host/port
|
||||
func connectToBonjourEndpoint(_ endpoint: NWEndpoint) {
|
||||
// Debug stuff
|
||||
endpoint.txtRecord?.forEach({ (key: String, value: NWTXTRecord.Entry) in
|
||||
print("\(key): \(value)")
|
||||
})
|
||||
|
||||
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 {
|
||||
let hostPortDebugOutput = "Connected to \(host):\(port)"
|
||||
|
||||
print(hostPortDebugOutput)
|
||||
|
||||
let hostString = "\(host)"
|
||||
let regex = try! Regex("%(.+)")
|
||||
let match = hostString.firstMatch(of: regex)
|
||||
let sanitizedHost = hostString.replacingOccurrences(of: match!.0, with: "")
|
||||
|
||||
print("[sanitized] Resolved \(sanitizedHost):\(port)")
|
||||
|
||||
connection?.cancel()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.socketHost = sanitizedHost
|
||||
self.socketPort = "\(port)"
|
||||
self.openWebsocket()
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
connection?.start(queue: .global())
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
socket?.disconnect()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private functions
|
||||
|
||||
// Opens the websocket connection
|
||||
private func openWebsocket() {
|
||||
//let fullUrlString = "http://\(socketHost):\(socketPort)/websocket"
|
||||
var request = URLRequest(url: URL(string: "http://\(socketHost):\(socketPort)/websocket")!)
|
||||
request.timeoutInterval = 5
|
||||
socket = WebSocket(request: request)
|
||||
socket!.delegate = self
|
||||
socket!.connect()
|
||||
}
|
||||
|
||||
private func reconnectWebsocket() {
|
||||
if socket == nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocket) {
|
||||
switch event {
|
||||
case .connected(let headers):
|
||||
isConnected = true
|
||||
print("websocket is connected: \(headers)")
|
||||
let jsonRpcRequest = JsonRpcRequest(method: "printer.objects.subscribe",
|
||||
params: ["objects":
|
||||
["extruder": nil,
|
||||
"virtual_sdcard": nil,
|
||||
"heater_bed": nil,
|
||||
"print_stats": nil]
|
||||
])
|
||||
|
||||
print(String(data: try! JSONEncoder().encode(jsonRpcRequest), encoding: .utf8)!)
|
||||
socket?.write(data: try! JSONEncoder().encode(jsonRpcRequest), completion: {
|
||||
print("[send] json-rpc printer.objects.subscribe query")
|
||||
})
|
||||
case .disconnected(let reason, let code):
|
||||
isConnected = false
|
||||
print("websocket is disconnected: \(reason) with code: \(code)")
|
||||
case .text(let string):
|
||||
// Check for initial RPC response
|
||||
let statusResponse = try? JSONDecoder().decode(jsonRpcResponse.self, from: Data(string.utf8))
|
||||
if let statusResponseSafe = statusResponse {
|
||||
self.parse_response(statusResponseSafe)
|
||||
}
|
||||
// Check for RPC updates
|
||||
if let updateResponse = try? JSONDecoder().decode(jsonRpcUpdate.self, from: Data(string.utf8)) {
|
||||
self.parse_update(updateResponse)
|
||||
}
|
||||
case .binary(let data):
|
||||
print("Received data: \(data.count)")
|
||||
case .ping(_):
|
||||
print("PING! \(Date())")
|
||||
// TODO: There's probably a better way to do this
|
||||
if(lastPingDate.addingTimeInterval(WEBSOCKET_TIMEOUT_INTERVAL) < Date.now) {
|
||||
print("Forcing reconnection of websocket..")
|
||||
self.reconnectWebsocket()
|
||||
}
|
||||
lastPingDate = Date()
|
||||
break
|
||||
case .pong(_):
|
||||
print("PONG!")
|
||||
break
|
||||
case .viabilityChanged(_):
|
||||
break
|
||||
case .reconnectSuggested(_):
|
||||
break
|
||||
case .cancelled:
|
||||
isConnected = false
|
||||
case .error(let error):
|
||||
isConnected = false
|
||||
print("[error] Starscream: \(error.debugDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user