Rename project to Soyuz
This commit is contained in:
		
							
								
								
									
										11
									
								
								Soyuz/Assets.xcassets/AccentColor.colorset/Contents.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Soyuz/Assets.xcassets/AccentColor.colorset/Contents.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "colors" : [
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "universal"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "info" : {
 | 
			
		||||
    "author" : "xcode",
 | 
			
		||||
    "version" : 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								Soyuz/Assets.xcassets/AppIcon.appiconset/Contents.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								Soyuz/Assets.xcassets/AppIcon.appiconset/Contents.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
{
 | 
			
		||||
  "images" : [
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "1x",
 | 
			
		||||
      "size" : "16x16"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "2x",
 | 
			
		||||
      "size" : "16x16"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "1x",
 | 
			
		||||
      "size" : "32x32"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "2x",
 | 
			
		||||
      "size" : "32x32"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "1x",
 | 
			
		||||
      "size" : "128x128"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "2x",
 | 
			
		||||
      "size" : "128x128"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "1x",
 | 
			
		||||
      "size" : "256x256"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "2x",
 | 
			
		||||
      "size" : "256x256"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "1x",
 | 
			
		||||
      "size" : "512x512"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "idiom" : "mac",
 | 
			
		||||
      "scale" : "2x",
 | 
			
		||||
      "size" : "512x512"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "info" : {
 | 
			
		||||
    "author" : "xcode",
 | 
			
		||||
    "version" : 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								Soyuz/Assets.xcassets/Contents.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								Soyuz/Assets.xcassets/Contents.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "info" : {
 | 
			
		||||
    "author" : "xcode",
 | 
			
		||||
    "version" : 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								Soyuz/Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Soyuz/Info.plist
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>CFBundleURLTypes</key>
 | 
			
		||||
	<array>
 | 
			
		||||
		<dict>
 | 
			
		||||
			<key>CFBundleTypeRole</key>
 | 
			
		||||
			<string>Viewer</string>
 | 
			
		||||
			<key>CFBundleURLName</key>
 | 
			
		||||
			<string>soyuz</string>
 | 
			
		||||
			<key>CFBundleURLSchemes</key>
 | 
			
		||||
			<array>
 | 
			
		||||
				<string>soyuz</string>
 | 
			
		||||
			</array>
 | 
			
		||||
		</dict>
 | 
			
		||||
	</array>
 | 
			
		||||
	<key>NSBonjourServices</key>
 | 
			
		||||
	<array>
 | 
			
		||||
		<string>_moonraker._tcp.</string>
 | 
			
		||||
		<string>_http._tcp.</string>
 | 
			
		||||
	</array>
 | 
			
		||||
	<key>NSServices</key>
 | 
			
		||||
	<array/>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
							
								
								
									
										8
									
								
								Soyuz/KlipperMon.xcdatamodeld/.xccurrentversion
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Soyuz/KlipperMon.xcdatamodeld/.xccurrentversion
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>_XCCurrentVersionName</key>
 | 
			
		||||
	<string>KlipperMon.xcdatamodel</string>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 | 
			
		||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
 | 
			
		||||
    <entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
 | 
			
		||||
        <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
 | 
			
		||||
    </entity>
 | 
			
		||||
    <elements>
 | 
			
		||||
        <element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
 | 
			
		||||
    </elements>
 | 
			
		||||
</model>
 | 
			
		||||
							
								
								
									
										56
									
								
								Soyuz/Persistence.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								Soyuz/Persistence.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
//
 | 
			
		||||
//  Persistence.swift
 | 
			
		||||
//  KlipperMon
 | 
			
		||||
//
 | 
			
		||||
//  Created by maddiefuzz on 2/7/23.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import CoreData
 | 
			
		||||
 | 
			
		||||
struct PersistenceController {
 | 
			
		||||
    static let shared = PersistenceController()
 | 
			
		||||
 | 
			
		||||
    static var preview: PersistenceController = {
 | 
			
		||||
        let result = PersistenceController(inMemory: true)
 | 
			
		||||
        let viewContext = result.container.viewContext
 | 
			
		||||
        for _ in 0..<10 {
 | 
			
		||||
            let newItem = Item(context: viewContext)
 | 
			
		||||
            newItem.timestamp = Date()
 | 
			
		||||
        }
 | 
			
		||||
        do {
 | 
			
		||||
            try viewContext.save()
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Replace this implementation with code to handle the error appropriately.
 | 
			
		||||
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
 | 
			
		||||
            let nsError = error as NSError
 | 
			
		||||
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
 | 
			
		||||
        }
 | 
			
		||||
        return result
 | 
			
		||||
    }()
 | 
			
		||||
 | 
			
		||||
    let container: NSPersistentContainer
 | 
			
		||||
 | 
			
		||||
    init(inMemory: Bool = false) {
 | 
			
		||||
        container = NSPersistentContainer(name: "KlipperMon")
 | 
			
		||||
        if inMemory {
 | 
			
		||||
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
 | 
			
		||||
        }
 | 
			
		||||
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
 | 
			
		||||
            if let error = error as NSError? {
 | 
			
		||||
                // Replace this implementation with code to handle the error appropriately.
 | 
			
		||||
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
 | 
			
		||||
 | 
			
		||||
                /*
 | 
			
		||||
                 Typical reasons for an error here include:
 | 
			
		||||
                 * The parent directory does not exist, cannot be created, or disallows writing.
 | 
			
		||||
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
 | 
			
		||||
                 * The device is out of space.
 | 
			
		||||
                 * The store could not be migrated to the current model version.
 | 
			
		||||
                 Check the error message to determine what the actual problem was.
 | 
			
		||||
                 */
 | 
			
		||||
                fatalError("Unresolved error \(error), \(error.userInfo)")
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        container.viewContext.automaticallyMergesChangesFromParent = true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "info" : {
 | 
			
		||||
    "author" : "xcode",
 | 
			
		||||
    "version" : 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										67
									
								
								Soyuz/PrinterConfigView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Soyuz/PrinterConfigView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
//
 | 
			
		||||
//  PrinterConfigView.swift
 | 
			
		||||
//  KlipperMon
 | 
			
		||||
//
 | 
			
		||||
//  Created by maddiefuzz on 2/8/23.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import Network
 | 
			
		||||
 | 
			
		||||
struct PrinterConfigView: View {
 | 
			
		||||
    @ObservedObject var printerManager = PrinterRequestManager.shared
 | 
			
		||||
    
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack {
 | 
			
		||||
            if(printerManager.isConnected) {
 | 
			
		||||
                HStack {
 | 
			
		||||
                    Image(systemName: "network")
 | 
			
		||||
                    Text(printerManager.connection.endpoint.toFriendlyString())
 | 
			
		||||
                    Text("\(printerManager.socketHost):\(printerManager.socketPort)")
 | 
			
		||||
                    Button {
 | 
			
		||||
                        printerManager.socket?.disconnect()
 | 
			
		||||
                    } label: {
 | 
			
		||||
                        Text("Disconnect")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .frame(width: 500, height: 80)
 | 
			
		||||
            } else {
 | 
			
		||||
                VStack {
 | 
			
		||||
                    Text("Auto-detected Printers")
 | 
			
		||||
                        .font(.title)
 | 
			
		||||
                    ForEach(printerManager.nwBrowserDiscoveredItems, id: \.hashValue) { result in
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Text(result.endpoint.toFriendlyString())
 | 
			
		||||
                            Button {
 | 
			
		||||
                                printerManager.resolveBonjourHost(result.endpoint)
 | 
			
		||||
                            } label: {
 | 
			
		||||
                                Text("Connect")
 | 
			
		||||
                                    .foregroundColor(.white)
 | 
			
		||||
                                    .padding()
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .frame(width: 500, height: 100)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .onAppear {
 | 
			
		||||
            NSApplication.shared.activate(ignoringOtherApps: true)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct PrinterConfigView_Previews: PreviewProvider {
 | 
			
		||||
    static var previews: some View {
 | 
			
		||||
        PrinterConfigView()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension NWEndpoint {
 | 
			
		||||
    func toFriendlyString() -> String {
 | 
			
		||||
        let regex = /\.(.+)/
 | 
			
		||||
        let match = self.debugDescription.firstMatch(of: regex)
 | 
			
		||||
        return self.debugDescription.replacingOccurrences(of: match!.0, with: "")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										92
									
								
								Soyuz/PrinterObjectsQuery.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								Soyuz/PrinterObjectsQuery.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
//
 | 
			
		||||
//  PrinterObjectsQuery.swift
 | 
			
		||||
//  KlipperMon
 | 
			
		||||
//
 | 
			
		||||
//  Created by maddiefuzz on 2/7/23.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
// Root struct to decode for REST response
 | 
			
		||||
struct PrinterObjectsQuery: Decodable {
 | 
			
		||||
    let result: ResultsData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ResultsData: Decodable {
 | 
			
		||||
    let eventtime: Double
 | 
			
		||||
    let status: StatusData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Individual update replies for JSON-RPC
 | 
			
		||||
struct jsonRpcUpdate: Decodable {
 | 
			
		||||
    let method: String?
 | 
			
		||||
    let params: jsonRpcParams
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct jsonRpcParams: Decodable {
 | 
			
		||||
    let status: StatusData?
 | 
			
		||||
    let timestamp: Double?
 | 
			
		||||
    
 | 
			
		||||
    init(from decoder: Decoder) throws {
 | 
			
		||||
        var container = try decoder.unkeyedContainer()
 | 
			
		||||
        self.status = try container.decode(StatusData.self)
 | 
			
		||||
        self.timestamp = try container.decode(Double.self)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
//
 | 
			
		||||
//    public init(from decoder: Decoder) throws {
 | 
			
		||||
//        let container = try decoder.container(keyedBy: CodingKeys.self)
 | 
			
		||||
//
 | 
			
		||||
//        do {
 | 
			
		||||
//            self = .statusData(try container.decode(StatusData.self, forKey: .statusData))
 | 
			
		||||
//        } catch DecodingError.keyNotFound {
 | 
			
		||||
//            print("Error")
 | 
			
		||||
//            self = .double(try container.decode(Double.self))
 | 
			
		||||
//            //self = .timestamp(try container.decode(Double.self, forKey: .timestamp))
 | 
			
		||||
//        }
 | 
			
		||||
//
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
// Root structs to decode for JSON-RPC response
 | 
			
		||||
struct jsonRpcResponse: Decodable  {
 | 
			
		||||
    let result: jsonRpcResult
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct jsonRpcResult: Decodable {
 | 
			
		||||
    let eventtime: Double
 | 
			
		||||
    let status: StatusData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Shared data sub-structs
 | 
			
		||||
struct StatusData: Decodable {
 | 
			
		||||
    let virtual_sdcard: VirtualSDCardData?
 | 
			
		||||
    let extruder: ExtruderData?
 | 
			
		||||
    let print_stats: PrintStatsData?
 | 
			
		||||
    let heater_bed: HeaterBedData?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct VirtualSDCardData: Decodable {
 | 
			
		||||
    let file_path: String?
 | 
			
		||||
    let progress: Double?
 | 
			
		||||
    let is_active: Bool?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ExtruderData: Decodable {
 | 
			
		||||
    let temperature: Double?
 | 
			
		||||
    let target: Double?
 | 
			
		||||
    let power: Double?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct PrintStatsData: Decodable {
 | 
			
		||||
    let filename: String?
 | 
			
		||||
    let print_duration: Double?
 | 
			
		||||
    let filament_used: Double?
 | 
			
		||||
    let state: String?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct HeaterBedData: Decodable {
 | 
			
		||||
    let temperature: Double?
 | 
			
		||||
    let target: Double?
 | 
			
		||||
    let power: Double?
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										273
									
								
								Soyuz/PrinterRequestManager.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								Soyuz/PrinterRequestManager.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,273 @@
 | 
			
		||||
//
 | 
			
		||||
//  PrinterRequestManager.swift
 | 
			
		||||
//  KlipperMon
 | 
			
		||||
//
 | 
			
		||||
//  Created by maddiefuzz on 2/7/23.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
import Network
 | 
			
		||||
import Starscream
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//@MainActor
 | 
			
		||||
class PrinterRequestManager: ObservableObject, WebSocketDelegate {
 | 
			
		||||
    let WEBSOCKET_TIMEOUT_INTERVAL: TimeInterval = 60.0
 | 
			
		||||
    
 | 
			
		||||
    static let shared = PrinterRequestManager()
 | 
			
		||||
    
 | 
			
		||||
    // Debug stuff
 | 
			
		||||
    let startDate = Date()
 | 
			
		||||
    let startDateString: String
 | 
			
		||||
    let filename: URL
 | 
			
		||||
    
 | 
			
		||||
    func writeToDebugLog(_ output: String) {
 | 
			
		||||
        do {
 | 
			
		||||
            let fileHandle = try FileHandle(forWritingTo: filename)
 | 
			
		||||
            defer {
 | 
			
		||||
                fileHandle.closeFile()
 | 
			
		||||
            }
 | 
			
		||||
            fileHandle.seekToEndOfFile()
 | 
			
		||||
            let dateFormatter = DateFormatter()
 | 
			
		||||
            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SS"
 | 
			
		||||
            
 | 
			
		||||
            let debugOutput = String("\(dateFormatter.string(from: Date())) - \(output)\n")
 | 
			
		||||
            fileHandle.write(debugOutput.data(using: .utf8)!)
 | 
			
		||||
        } catch {
 | 
			
		||||
            print("[error] writeToDebugLog - \(error)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Websocket JSON-RPC endpoints discovered via bonjour
 | 
			
		||||
    @Published var nwBrowserDiscoveredItems: [NWBrowser.Result] = []
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    // 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
 | 
			
		||||
    
 | 
			
		||||
    let nwBrowser = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)
 | 
			
		||||
    var connection: NWConnection!
 | 
			
		||||
    
 | 
			
		||||
    var socket: WebSocket?
 | 
			
		||||
    var lastPingDate = Date()
 | 
			
		||||
    
 | 
			
		||||
    // TODO: Set this up to actually reconnect
 | 
			
		||||
    
 | 
			
		||||
    // 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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private init() {
 | 
			
		||||
        state = ""
 | 
			
		||||
        progress = 0.0
 | 
			
		||||
        extruderTemperature = 0.0
 | 
			
		||||
        bedTemperature = 0.0
 | 
			
		||||
        socketHost = ""
 | 
			
		||||
        socketPort = ""
 | 
			
		||||
        //reconnectionTimer = nil
 | 
			
		||||
        
 | 
			
		||||
        // MARK: Debug stuff
 | 
			
		||||
        startDateString = "\(startDate)\n\n"
 | 
			
		||||
        filename = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("klippermon-debug-\(startDateString).txt")
 | 
			
		||||
        
 | 
			
		||||
        do {
 | 
			
		||||
            try startDateString.write(to: filename, atomically: true, encoding: .utf8)
 | 
			
		||||
        } catch {
 | 
			
		||||
            print("[error] Couldn't write to \(filename) - \(error)")
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // MARK: Bonjour browser initialization at instantiation
 | 
			
		||||
        nwBrowser.browseResultsChangedHandler = { (newResults, changes) in
 | 
			
		||||
            print("[update] Results changed.")
 | 
			
		||||
            newResults.forEach { result in
 | 
			
		||||
                print(result)
 | 
			
		||||
                self.nwBrowserDiscoveredItems.append(result)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // Bonjour browser state update handler
 | 
			
		||||
        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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // Start up the bonjour browser, get results and process them in the update handler
 | 
			
		||||
        nwBrowser.start(queue: DispatchQueue.main)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 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 resolveBonjourHost(_ endpoint: NWEndpoint) {
 | 
			
		||||
        // Debug stuff
 | 
			
		||||
        endpoint.txtRecord?.forEach({ (key: String, value: NWTXTRecord.Entry) in
 | 
			
		||||
            print("\(key): \(value)")
 | 
			
		||||
        })
 | 
			
		||||
        
 | 
			
		||||
        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)
 | 
			
		||||
                    writeToDebugLog(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)")
 | 
			
		||||
                    
 | 
			
		||||
                    socketHost = sanitizedHost
 | 
			
		||||
                    socketPort = "\(port)"
 | 
			
		||||
                    connection.cancel()
 | 
			
		||||
                    self.openWebsocket()
 | 
			
		||||
                }
 | 
			
		||||
            default:
 | 
			
		||||
                break
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        connection.start(queue: .global())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func reconnectWebsocket() {
 | 
			
		||||
        if socket == nil {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        socket!.disconnect()
 | 
			
		||||
        self.openWebsocket()
 | 
			
		||||
        //socket!.write(ping: "PING!".data(using: .utf8)!)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Opens the websocket connection
 | 
			
		||||
    // TODO: host and port should be function arguments probably maybe
 | 
			
		||||
    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()
 | 
			
		||||
        
 | 
			
		||||
        // TODO: Check that this keeps the connection alive properly
 | 
			
		||||
        Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [self] timer in
 | 
			
		||||
            //self.checkWebsocketIsAlive()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // MARK: delegate callback for Starscream WebSocketClient
 | 
			
		||||
    func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocket) {
 | 
			
		||||
        switch event {
 | 
			
		||||
        case .connected(let headers):
 | 
			
		||||
            isConnected = true
 | 
			
		||||
            print("websocket is connected: \(headers)")
 | 
			
		||||
            writeToDebugLog("Connected to WebSocket")
 | 
			
		||||
            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)")
 | 
			
		||||
            writeToDebugLog("Websocket is disconnected: \(reason) with code: \(code)")
 | 
			
		||||
        case .text(let string):
 | 
			
		||||
            self.writeToDebugLog(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):
 | 
			
		||||
            self.writeToDebugLog(String(data: data, encoding: .utf8)!)
 | 
			
		||||
            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)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								Soyuz/PrinterStats.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								Soyuz/PrinterStats.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
//
 | 
			
		||||
//  PrinterStats.swift
 | 
			
		||||
//  KlipperMon
 | 
			
		||||
//
 | 
			
		||||
//  Created by maddiefuzz on 2/9/23.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
class PrinterStats: ObservableObject {
 | 
			
		||||
    @Published var state: String
 | 
			
		||||
    @Published var progress: Double
 | 
			
		||||
    @Published var extruderTemperature: Double
 | 
			
		||||
    @Published var bedTemperature: Double
 | 
			
		||||
    
 | 
			
		||||
    init(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)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func update(update: jsonRpcUpdate) {
 | 
			
		||||
//        print(update)
 | 
			
		||||
        if let newState = update.params.status?.print_stats?.state {
 | 
			
		||||
            //state = update.params[0].print_stats?.state
 | 
			
		||||
            state = newState
 | 
			
		||||
        }
 | 
			
		||||
        if let newProgress = update.params.status?.virtual_sdcard?.progress  {
 | 
			
		||||
            print("Update progress")
 | 
			
		||||
            progress = newProgress
 | 
			
		||||
        }
 | 
			
		||||
        if let newExtruderTemp = update.params.status?.extruder?.temperature  {
 | 
			
		||||
            print("Update extruder temp \(newExtruderTemp)")
 | 
			
		||||
            extruderTemperature = newExtruderTemp
 | 
			
		||||
        }
 | 
			
		||||
        if let newBedTemp = update.params.status?.heater_bed?.temperature  {
 | 
			
		||||
            print("Update heated bed \(newBedTemp)")
 | 
			
		||||
            bedTemperature = newBedTemp
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								Soyuz/Soyuz.entitlements
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Soyuz/Soyuz.entitlements
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>com.apple.security.app-sandbox</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
	<key>com.apple.security.files.user-selected.read-only</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
	<key>com.apple.security.network.client</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
</dict>
 | 
			
		||||
</plist>
 | 
			
		||||
							
								
								
									
										35
									
								
								Soyuz/SoyuzApp.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Soyuz/SoyuzApp.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
//
 | 
			
		||||
//  KlipperMonApp.swift
 | 
			
		||||
//  KlipperMon
 | 
			
		||||
//
 | 
			
		||||
//  Created by maddiefuzz on 2/7/23.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
 | 
			
		||||
@main
 | 
			
		||||
struct SoyuzApp: App {
 | 
			
		||||
    let persistenceController = PersistenceController.shared
 | 
			
		||||
    
 | 
			
		||||
    @State var currentIcon = "move.3d"
 | 
			
		||||
    
 | 
			
		||||
    var body: some Scene {
 | 
			
		||||
//        WindowGroup(id: "floating-stats") {
 | 
			
		||||
//            KlipperMonMenuBarExtraView(currentMenuBarIcon: $currentIcon)
 | 
			
		||||
//                .environment(\.managedObjectContext, persistenceController.container.viewContext)
 | 
			
		||||
//        }
 | 
			
		||||
        
 | 
			
		||||
        WindowGroup("Configuration", id: "soyuz_cfg", content: {
 | 
			
		||||
            PrinterConfigView()
 | 
			
		||||
                //.frame(minWidth: 300, maxWidth: 600, minHeight: 60, maxHeight: 100)
 | 
			
		||||
        })
 | 
			
		||||
        .windowResizability(.contentSize)
 | 
			
		||||
        
 | 
			
		||||
        MenuBarExtra("Soyuz", systemImage: currentIcon) {
 | 
			
		||||
            SoyuzMenuBarExtraView(currentMenuBarIcon: $currentIcon)
 | 
			
		||||
                .padding([.top, .leading, .trailing], 8)
 | 
			
		||||
                .padding([.bottom], 6)
 | 
			
		||||
        }
 | 
			
		||||
        .menuBarExtraStyle(.window)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										107
									
								
								Soyuz/SoyuzMenuBarExtraView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								Soyuz/SoyuzMenuBarExtraView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
//
 | 
			
		||||
//  KlipperMonMenuBarExtraView.swift
 | 
			
		||||
//  KlipperMon
 | 
			
		||||
//
 | 
			
		||||
//  Created by maddiefuzz on 2/7/23.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import SwiftUI
 | 
			
		||||
import AppKit
 | 
			
		||||
import Network
 | 
			
		||||
 | 
			
		||||
struct SoyuzMenuBarExtraView: View {
 | 
			
		||||
    // The threshhold considered a burn-risk, at which point certain UI elements turn red.
 | 
			
		||||
    let DANGERTEMP = 40.0
 | 
			
		||||
    
 | 
			
		||||
    @Environment(\.openWindow) var openWindow
 | 
			
		||||
    
 | 
			
		||||
    @ObservedObject var printerManager = PrinterRequestManager.shared
 | 
			
		||||
    
 | 
			
		||||
    @State var printPercentage: Double = 0
 | 
			
		||||
    
 | 
			
		||||
    @Binding var currentMenuBarIcon: String
 | 
			
		||||
    
 | 
			
		||||
    @State var hotendHotTemp: Bool = false
 | 
			
		||||
    @State var bedHotTemp: Bool = false
 | 
			
		||||
    
 | 
			
		||||
    // TODO: Use @published API data instead of instance state variable
 | 
			
		||||
    var body: some View {
 | 
			
		||||
        VStack {
 | 
			
		||||
            // Printer Readouts
 | 
			
		||||
            //if let printerStats = printerManager.printerStats {
 | 
			
		||||
            if(printerManager.isConnected) {
 | 
			
		||||
                VStack {
 | 
			
		||||
                    Text(printerManager.state.capitalized)
 | 
			
		||||
                        .font(.title)
 | 
			
		||||
                        .padding(4)
 | 
			
		||||
                    // Print information
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        Image(systemName: "pencil.tip")
 | 
			
		||||
                            .rotationEffect(Angle(degrees: 180))
 | 
			
		||||
                            .offset(x: 5.5, y: 4)
 | 
			
		||||
                            .font(.system(size: 24))
 | 
			
		||||
                        ProgressView(value: printerManager.progress, total: 1.0)
 | 
			
		||||
                            .progressViewStyle(.linear)
 | 
			
		||||
                            .offset(x: 10)
 | 
			
		||||
                        Text("\(Int(printerManager.progress * 100))%")
 | 
			
		||||
                            .padding(2)
 | 
			
		||||
                            .padding([.leading], 8)
 | 
			
		||||
                    }
 | 
			
		||||
                    // Temperatures
 | 
			
		||||
                    HStack {
 | 
			
		||||
                        // Hot-end temperature
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Image(systemName: "flame")
 | 
			
		||||
                                .foregroundColor( printerManager.extruderTemperature > DANGERTEMP ? .red : .white )
 | 
			
		||||
                                .opacity( printerManager.extruderTemperature > DANGERTEMP ? 1.0 : 0.3 )
 | 
			
		||||
                            Text("Hotend")
 | 
			
		||||
                                .font(.headline)
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Text("\(Int(printerManager.extruderTemperature))°C")
 | 
			
		||||
                        }
 | 
			
		||||
                        // Bed temperature
 | 
			
		||||
                        HStack {
 | 
			
		||||
                            Image(systemName: "flame")
 | 
			
		||||
                                .foregroundColor( printerManager.bedTemperature > DANGERTEMP ? .red : .white )
 | 
			
		||||
                                .opacity( printerManager.bedTemperature > DANGERTEMP ? 1.0 : 0.3 )
 | 
			
		||||
                            Text("Plate")
 | 
			
		||||
                                .font(.headline)
 | 
			
		||||
                            Spacer()
 | 
			
		||||
                            Text("\(Int(printerManager.bedTemperature))°C")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Divider()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        //.frame(minWidth: 220, minHeight: 100)
 | 
			
		||||
        // Footer information
 | 
			
		||||
        HStack {
 | 
			
		||||
            Button {
 | 
			
		||||
                print("Button pressed")
 | 
			
		||||
                openWindow(id: "soyuz_cfg")
 | 
			
		||||
            } label: {
 | 
			
		||||
                Text("Printers")
 | 
			
		||||
                    .foregroundColor(.white)
 | 
			
		||||
            }
 | 
			
		||||
            Spacer()
 | 
			
		||||
            if(printerManager.isConnected) {
 | 
			
		||||
                Image(systemName: "network")
 | 
			
		||||
                Text("Online")
 | 
			
		||||
            } else {
 | 
			
		||||
                Image(systemName: "exclamationmark.triangle")
 | 
			
		||||
                Text("Offline")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .padding(2)
 | 
			
		||||
        .frame(minWidth: 220, maxWidth: 375)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct KlipperMonMenuBarExtraView_Previews: PreviewProvider {
 | 
			
		||||
    @State static var currentMenuBarIcon = "move.3d"
 | 
			
		||||
    static var previews: some View {
 | 
			
		||||
        SoyuzMenuBarExtraView(currentMenuBarIcon: $currentMenuBarIcon)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user