Compare commits

...

10 Commits

70 changed files with 1495 additions and 647 deletions

View File

@ -1,58 +0,0 @@
{
"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
}
}

View File

@ -1,35 +0,0 @@
//
// KlipperMonApp.swift
// KlipperMon
//
// Created by maddiefuzz on 2/7/23.
//
import SwiftUI
@main
struct KlipperMonMenuBarApp: 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)
//.frame(width: 300, height: 140)
}
//.windowResizability(.contentSize)
Window("Configuration", id: "soyuz_cfg", content: {
PrinterConfigView()
})
MenuBarExtra("Soyuz", systemImage: currentIcon) {
KlipperMonMenuBarExtraView(currentMenuBarIcon: $currentIcon)
.padding([.top, .leading, .trailing], 8)
.padding([.bottom], 6)
}
.menuBarExtraStyle(.window)
}
}

View File

@ -1,142 +0,0 @@
//
// KlipperMonMenuBarExtraView.swift
// KlipperMon
//
// Created by maddiefuzz on 2/7/23.
//
import SwiftUI
import AppKit
struct KlipperMonMenuBarExtraView: View {
let DANGERTEMP = 40.0
@Environment(\.openWindow) var openWindow
@ObservedObject var printerManager = PrinterRequestManager.shared
@State var printerObjectsQuery: PrinterObjectsQuery?
@State var printPercentage: Double = 0
// TODO: Don't forget, create @State variable for printer status (i.e. "Printing", etc)
// and programmatically add a "connecting" section
@State var printerStatus: String = ""
@Binding var currentMenuBarIcon: String
@State var hotendHotTemp: Bool = false
@State var bedHotTemp: Bool = false
//@State var nwBrowserDiscoveredItems: [NWEndpoint] = []
//var nwBrowser = NWBrowser(for: .bonjour(type: "_moonraker._tcp.", domain: "local."), using: .tcp)
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
// TODO: Use @published API data instead of instance state variable
var body: some View {
VStack {
// Printer Readouts
if let queryResults = printerManager.printerObjectsQuery {
Text(queryResults.result.status.print_stats.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: queryResults.result.status.virtual_sdcard.progress, total: 1.0)
.progressViewStyle(.linear)
.offset(x: 10)
Text("\(Int(queryResults.result.status.virtual_sdcard.progress * 100))%")
.padding(2)
.padding([.leading], 8)
}
// Temperatures
HStack {
// Hot-end temperature
HStack {
Image(systemName: "flame")
.foregroundColor( hotendHotTemp ? .red : .white )
.opacity( hotendHotTemp ? 1.0 : 0.3 )
Text("Hotend")
.font(.headline)
Spacer()
Text("\(Int(queryResults.result.status.extruder.temperature))°C")
}
// Bed temperature
HStack {
Image(systemName: "flame")
.foregroundColor( bedHotTemp ? .red : .white )
.opacity( bedHotTemp ? 1.0 : 0.3 )
Text("Plate")
.font(.headline)
Spacer()
Text("\(Int(queryResults.result.status.heater_bed.temperature))°C")
}
}
Divider()
}
}
.frame(minWidth: 220, minHeight: 100)
// .overlay {
// if !printerManager.printerCommsOkay {
// RoundedRectangle(cornerRadius: 10, style: .circular)
// .foregroundColor(.black)
// .frame(minWidth: 300, minHeight: 100)
// .opacity(0.6)
// }
// }
// Footer information
HStack {
Button {
print("Button pressed")
openWindow(id: "soyuz_cfg")
} label: {
Text("Server Config")
.foregroundColor(.white)
}
Spacer()
if(printerManager.printerCommsOkay) {
Image(systemName: "network")
Text("Online")
} else {
Image(systemName: "xmark")
Text("Offline")
}
}
.padding(4)
.frame(minWidth: 220, maxWidth: 250)
.onReceive(timer) { input in
Task {
//await printerManager.queryPrinterStats()
if let query = printerManager.printerObjectsQuery {
hotendHotTemp = (query.result.status.extruder.temperature > DANGERTEMP) ? true : false
bedHotTemp = (query.result.status.heater_bed.temperature > DANGERTEMP) ? true : false
printerStatus = query.result.status.print_stats.state.capitalized
} else {
printerStatus = "Connecting..."
}
}
}
// Testing bonjour stuff
ForEach(printerManager.nwBrowserDiscoveredItems, id: \.hashValue) { endpoint in
Button {
printerManager.resolveBonjourHost(endpoint)
} label: {
Text(endpoint.debugDescription)
}
}
}
}
struct KlipperMonMenuBarExtraView_Previews: PreviewProvider {
@State static var currentMenuBarIcon = "move.3d"
static var previews: some View {
KlipperMonMenuBarExtraView(currentMenuBarIcon: $currentMenuBarIcon)
}
}

View File

@ -1,20 +0,0 @@
//
// PrinterConfigView.swift
// KlipperMon
//
// Created by maddiefuzz on 2/8/23.
//
import SwiftUI
struct PrinterConfigView: View {
var body: some View {
Text("Config Printer In Here")
}
}
struct PrinterConfigView_Previews: PreviewProvider {
static var previews: some View {
PrinterConfigView()
}
}

View File

@ -1,49 +0,0 @@
//
// PrinterObjectsQuery.swift
// KlipperMon
//
// Created by maddiefuzz on 2/7/23.
//
import Foundation
struct PrinterObjectsQuery: Decodable {
let result: ResultsData
}
struct ResultsData: Decodable {
let eventtime: Double
let status: StatusData
}
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
}

View File

@ -1,176 +0,0 @@
//
// 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)
}
}
class PrinterRequestManager: ObservableObject, WebSocketDelegate {
func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocket) {
switch event {
case .connected(let headers):
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("Data transferred.")
})
case .disconnected(let reason, let code):
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:
break
case .error(let error):
print("[error] Starscream: \(error.debugDescription)")
}
}
// 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()
private init() {
// 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.endpoint)
}
}
// State update handler
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
}
}
// Start up the bonjour browser, get results and process them in the update handler
nwBrowser.start(queue: DispatchQueue.main)
}
// Called from the UI, providing 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) {
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())
}
// Opens the websocket connection
// TODO: host and port should be function arguments probably maybe
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()
}
}
// Old REST way to do it
// TODO: Stop using this.
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")
}
let urlRequest = URLRequest(url: url)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print("Error with response.")
return
}
// handle data as JSON
let decoder = JSONDecoder()
printerObjectsQuery = try decoder.decode(PrinterObjectsQuery.self, from: data)
printerCommsOkay = true
} catch {
print("Exception thrown: \(error)")
printerCommsOkay = false
}
}
}

View File

@ -1,36 +0,0 @@
//
// KlipperMonTests.swift
// KlipperMonTests
//
// Created by maddiefuzz on 2/7/23.
//
import XCTest
@testable import KlipperMon
final class KlipperMonTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

View File

@ -1,32 +0,0 @@
//
// KlipperMonUITestsLaunchTests.swift
// KlipperMonUITests
//
// Created by maddiefuzz on 2/7/23.
//
import XCTest
final class KlipperMonUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

View File

@ -8,17 +8,20 @@
/* Begin PBXBuildFile section */
E124B9D929941A4D00C0D2D2 /* PrinterConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E124B9D829941A4D00C0D2D2 /* PrinterConfigView.swift */; };
E180B5E92992CD9100425DB0 /* KlipperMonApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B5E82992CD9100425DB0 /* KlipperMonApp.swift */; };
E16378B429A491E6002F05E9 /* MoonrakerSocketManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16378B329A491E6002F05E9 /* MoonrakerSocketManagerTests.swift */; };
E180B5E92992CD9100425DB0 /* SoyuzApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B5E82992CD9100425DB0 /* SoyuzApp.swift */; };
E180B5ED2992CD9200425DB0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E180B5EC2992CD9200425DB0 /* Assets.xcassets */; };
E180B5F02992CD9200425DB0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E180B5EF2992CD9200425DB0 /* Preview Assets.xcassets */; };
E180B5F22992CD9200425DB0 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B5F12992CD9200425DB0 /* Persistence.swift */; };
E180B5F52992CD9200425DB0 /* KlipperMon.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E180B5F32992CD9200425DB0 /* KlipperMon.xcdatamodeld */; };
E180B6002992CD9300425DB0 /* KlipperMonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B5FF2992CD9300425DB0 /* KlipperMonTests.swift */; };
E180B60A2992CD9300425DB0 /* KlipperMonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B6092992CD9300425DB0 /* KlipperMonUITests.swift */; };
E180B60C2992CD9300425DB0 /* KlipperMonUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B60B2992CD9300425DB0 /* KlipperMonUITestsLaunchTests.swift */; };
E180B6002992CD9300425DB0 /* SoyuzTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B5FF2992CD9300425DB0 /* SoyuzTests.swift */; };
E180B60A2992CD9300425DB0 /* SoyuzUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B6092992CD9300425DB0 /* SoyuzUITests.swift */; };
E180B60C2992CD9300425DB0 /* SoyuzUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B60B2992CD9300425DB0 /* SoyuzUITestsLaunchTests.swift */; };
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 */; };
E180B61F2992DBB000425DB0 /* SoyuzMenuBarExtraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B61E2992DBB000425DB0 /* SoyuzMenuBarExtraView.swift */; };
E180B6222993256E00425DB0 /* MoonrakerSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E180B6212993256E00425DB0 /* MoonrakerSocketManager.swift */; };
E1A93C6729C932E200BAE750 /* BonjourBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A93C6629C932E200BAE750 /* BonjourBrowser.swift */; };
E1A93C6929CD627100BAE750 /* BonjourBrowserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A93C6829CD627100BAE750 /* BonjourBrowserTests.swift */; };
E1E8B07729949E2700BABE4B /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = E1E8B07629949E2700BABE4B /* Starscream */; };
/* End PBXBuildFile section */
@ -42,21 +45,24 @@
/* Begin PBXFileReference section */
E124B9D72993FE5500C0D2D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
E124B9D829941A4D00C0D2D2 /* PrinterConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrinterConfigView.swift; sourceTree = "<group>"; };
E180B5E52992CD9100425DB0 /* KlipperMon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KlipperMon.app; sourceTree = BUILT_PRODUCTS_DIR; };
E180B5E82992CD9100425DB0 /* KlipperMonApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlipperMonApp.swift; sourceTree = "<group>"; };
E16378B329A491E6002F05E9 /* MoonrakerSocketManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoonrakerSocketManagerTests.swift; sourceTree = "<group>"; };
E180B5E52992CD9100425DB0 /* Soyuz.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Soyuz.app; sourceTree = BUILT_PRODUCTS_DIR; };
E180B5E82992CD9100425DB0 /* SoyuzApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoyuzApp.swift; sourceTree = "<group>"; };
E180B5EC2992CD9200425DB0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
E180B5EF2992CD9200425DB0 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
E180B5F12992CD9200425DB0 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
E180B5F42992CD9200425DB0 /* KlipperMon.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = KlipperMon.xcdatamodel; sourceTree = "<group>"; };
E180B5F62992CD9200425DB0 /* KlipperMon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KlipperMon.entitlements; sourceTree = "<group>"; };
E180B5FB2992CD9300425DB0 /* KlipperMonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KlipperMonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E180B5FF2992CD9300425DB0 /* KlipperMonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlipperMonTests.swift; sourceTree = "<group>"; };
E180B6052992CD9300425DB0 /* KlipperMonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KlipperMonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E180B6092992CD9300425DB0 /* KlipperMonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlipperMonUITests.swift; sourceTree = "<group>"; };
E180B60B2992CD9300425DB0 /* KlipperMonUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlipperMonUITestsLaunchTests.swift; sourceTree = "<group>"; };
E180B5F62992CD9200425DB0 /* Soyuz.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Soyuz.entitlements; sourceTree = "<group>"; };
E180B5FB2992CD9300425DB0 /* SoyuzTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SoyuzTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E180B5FF2992CD9300425DB0 /* SoyuzTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoyuzTests.swift; sourceTree = "<group>"; };
E180B6052992CD9300425DB0 /* SoyuzUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SoyuzUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E180B6092992CD9300425DB0 /* SoyuzUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoyuzUITests.swift; sourceTree = "<group>"; };
E180B60B2992CD9300425DB0 /* SoyuzUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoyuzUITestsLaunchTests.swift; sourceTree = "<group>"; };
E180B61C2992D53700425DB0 /* PrinterObjectsQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrinterObjectsQuery.swift; sourceTree = "<group>"; };
E180B61E2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlipperMonMenuBarExtraView.swift; sourceTree = "<group>"; };
E180B6212993256E00425DB0 /* PrinterRequestManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrinterRequestManager.swift; sourceTree = "<group>"; };
E180B61E2992DBB000425DB0 /* SoyuzMenuBarExtraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoyuzMenuBarExtraView.swift; sourceTree = "<group>"; };
E180B6212993256E00425DB0 /* MoonrakerSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoonrakerSocketManager.swift; sourceTree = "<group>"; };
E1A93C6629C932E200BAE750 /* BonjourBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonjourBrowser.swift; sourceTree = "<group>"; };
E1A93C6829CD627100BAE750 /* BonjourBrowserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BonjourBrowserTests.swift; path = SoyuzTests/BonjourBrowserTests.swift; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -88,9 +94,9 @@
E180B5DC2992CD9100425DB0 = {
isa = PBXGroup;
children = (
E180B5E72992CD9100425DB0 /* KlipperMon */,
E180B5FE2992CD9300425DB0 /* KlipperMonTests */,
E180B6082992CD9300425DB0 /* KlipperMonUITests */,
E180B5E72992CD9100425DB0 /* Soyuz */,
E180B5FE2992CD9300425DB0 /* SoyuzTests */,
E180B6082992CD9300425DB0 /* SoyuzUITests */,
E180B5E62992CD9100425DB0 /* Products */,
);
sourceTree = "<group>";
@ -98,29 +104,27 @@
E180B5E62992CD9100425DB0 /* Products */ = {
isa = PBXGroup;
children = (
E180B5E52992CD9100425DB0 /* KlipperMon.app */,
E180B5FB2992CD9300425DB0 /* KlipperMonTests.xctest */,
E180B6052992CD9300425DB0 /* KlipperMonUITests.xctest */,
E180B5E52992CD9100425DB0 /* Soyuz.app */,
E180B5FB2992CD9300425DB0 /* SoyuzTests.xctest */,
E180B6052992CD9300425DB0 /* SoyuzUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
E180B5E72992CD9100425DB0 /* KlipperMon */ = {
E180B5E72992CD9100425DB0 /* Soyuz */ = {
isa = PBXGroup;
children = (
E1A93C6529C92B3500BAE750 /* ViewModels */,
E1A93C6429C92AEE00BAE750 /* Views */,
E124B9D72993FE5500C0D2D2 /* Info.plist */,
E180B5E82992CD9100425DB0 /* KlipperMonApp.swift */,
E180B5E82992CD9100425DB0 /* SoyuzApp.swift */,
E180B5EC2992CD9200425DB0 /* Assets.xcassets */,
E180B5F12992CD9200425DB0 /* Persistence.swift */,
E180B5F62992CD9200425DB0 /* KlipperMon.entitlements */,
E180B5F62992CD9200425DB0 /* Soyuz.entitlements */,
E180B5F32992CD9200425DB0 /* KlipperMon.xcdatamodeld */,
E180B5EE2992CD9200425DB0 /* Preview Content */,
E180B61C2992D53700425DB0 /* PrinterObjectsQuery.swift */,
E180B61E2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift */,
E180B6212993256E00425DB0 /* PrinterRequestManager.swift */,
E124B9D829941A4D00C0D2D2 /* PrinterConfigView.swift */,
);
path = KlipperMon;
path = Soyuz;
sourceTree = "<group>";
};
E180B5EE2992CD9200425DB0 /* Preview Content */ = {
@ -131,29 +135,50 @@
path = "Preview Content";
sourceTree = "<group>";
};
E180B5FE2992CD9300425DB0 /* KlipperMonTests */ = {
E180B5FE2992CD9300425DB0 /* SoyuzTests */ = {
isa = PBXGroup;
children = (
E180B5FF2992CD9300425DB0 /* KlipperMonTests.swift */,
E180B5FF2992CD9300425DB0 /* SoyuzTests.swift */,
E1A93C6829CD627100BAE750 /* BonjourBrowserTests.swift */,
E16378B329A491E6002F05E9 /* MoonrakerSocketManagerTests.swift */,
);
path = KlipperMonTests;
path = SoyuzTests;
sourceTree = "<group>";
};
E180B6082992CD9300425DB0 /* KlipperMonUITests */ = {
E180B6082992CD9300425DB0 /* SoyuzUITests */ = {
isa = PBXGroup;
children = (
E180B6092992CD9300425DB0 /* KlipperMonUITests.swift */,
E180B60B2992CD9300425DB0 /* KlipperMonUITestsLaunchTests.swift */,
E180B6092992CD9300425DB0 /* SoyuzUITests.swift */,
E180B60B2992CD9300425DB0 /* SoyuzUITestsLaunchTests.swift */,
);
path = KlipperMonUITests;
path = SoyuzUITests;
sourceTree = "<group>";
};
E1A93C6429C92AEE00BAE750 /* Views */ = {
isa = PBXGroup;
children = (
E180B61E2992DBB000425DB0 /* SoyuzMenuBarExtraView.swift */,
E124B9D829941A4D00C0D2D2 /* PrinterConfigView.swift */,
);
path = Views;
sourceTree = "<group>";
};
E1A93C6529C92B3500BAE750 /* ViewModels */ = {
isa = PBXGroup;
children = (
E180B61C2992D53700425DB0 /* PrinterObjectsQuery.swift */,
E180B6212993256E00425DB0 /* MoonrakerSocketManager.swift */,
E1A93C6629C932E200BAE750 /* BonjourBrowser.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
E180B5E42992CD9100425DB0 /* KlipperMon */ = {
E180B5E42992CD9100425DB0 /* Soyuz */ = {
isa = PBXNativeTarget;
buildConfigurationList = E180B60F2992CD9300425DB0 /* Build configuration list for PBXNativeTarget "KlipperMon" */;
buildConfigurationList = E180B60F2992CD9300425DB0 /* Build configuration list for PBXNativeTarget "Soyuz" */;
buildPhases = (
E180B5E12992CD9100425DB0 /* Sources */,
E180B5E22992CD9100425DB0 /* Frameworks */,
@ -163,17 +188,17 @@
);
dependencies = (
);
name = KlipperMon;
name = Soyuz;
packageProductDependencies = (
E1E8B07629949E2700BABE4B /* Starscream */,
);
productName = KlipperMon;
productReference = E180B5E52992CD9100425DB0 /* KlipperMon.app */;
productReference = E180B5E52992CD9100425DB0 /* Soyuz.app */;
productType = "com.apple.product-type.application";
};
E180B5FA2992CD9300425DB0 /* KlipperMonTests */ = {
E180B5FA2992CD9300425DB0 /* SoyuzTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = E180B6122992CD9300425DB0 /* Build configuration list for PBXNativeTarget "KlipperMonTests" */;
buildConfigurationList = E180B6122992CD9300425DB0 /* Build configuration list for PBXNativeTarget "SoyuzTests" */;
buildPhases = (
E180B5F72992CD9300425DB0 /* Sources */,
E180B5F82992CD9300425DB0 /* Frameworks */,
@ -184,14 +209,14 @@
dependencies = (
E180B5FD2992CD9300425DB0 /* PBXTargetDependency */,
);
name = KlipperMonTests;
name = SoyuzTests;
productName = KlipperMonTests;
productReference = E180B5FB2992CD9300425DB0 /* KlipperMonTests.xctest */;
productReference = E180B5FB2992CD9300425DB0 /* SoyuzTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
E180B6042992CD9300425DB0 /* KlipperMonUITests */ = {
E180B6042992CD9300425DB0 /* SoyuzUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = E180B6152992CD9300425DB0 /* Build configuration list for PBXNativeTarget "KlipperMonUITests" */;
buildConfigurationList = E180B6152992CD9300425DB0 /* Build configuration list for PBXNativeTarget "SoyuzUITests" */;
buildPhases = (
E180B6012992CD9300425DB0 /* Sources */,
E180B6022992CD9300425DB0 /* Frameworks */,
@ -202,9 +227,9 @@
dependencies = (
E180B6072992CD9300425DB0 /* PBXTargetDependency */,
);
name = KlipperMonUITests;
name = SoyuzUITests;
productName = KlipperMonUITests;
productReference = E180B6052992CD9300425DB0 /* KlipperMonUITests.xctest */;
productReference = E180B6052992CD9300425DB0 /* SoyuzUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
@ -230,7 +255,7 @@
};
};
};
buildConfigurationList = E180B5E02992CD9100425DB0 /* Build configuration list for PBXProject "KlipperMon" */;
buildConfigurationList = E180B5E02992CD9100425DB0 /* Build configuration list for PBXProject "Soyuz" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
@ -246,9 +271,9 @@
projectDirPath = "";
projectRoot = "";
targets = (
E180B5E42992CD9100425DB0 /* KlipperMon */,
E180B5FA2992CD9300425DB0 /* KlipperMonTests */,
E180B6042992CD9300425DB0 /* KlipperMonUITests */,
E180B5E42992CD9100425DB0 /* Soyuz */,
E180B5FA2992CD9300425DB0 /* SoyuzTests */,
E180B6042992CD9300425DB0 /* SoyuzUITests */,
);
};
/* End PBXProject section */
@ -288,9 +313,10 @@
E180B5F52992CD9200425DB0 /* KlipperMon.xcdatamodeld in Sources */,
E124B9D929941A4D00C0D2D2 /* PrinterConfigView.swift in Sources */,
E180B5F22992CD9200425DB0 /* Persistence.swift in Sources */,
E180B5E92992CD9100425DB0 /* KlipperMonApp.swift in Sources */,
E180B6222993256E00425DB0 /* PrinterRequestManager.swift in Sources */,
E180B61F2992DBB000425DB0 /* KlipperMonMenuBarExtraView.swift in Sources */,
E1A93C6729C932E200BAE750 /* BonjourBrowser.swift in Sources */,
E180B5E92992CD9100425DB0 /* SoyuzApp.swift in Sources */,
E180B6222993256E00425DB0 /* MoonrakerSocketManager.swift in Sources */,
E180B61F2992DBB000425DB0 /* SoyuzMenuBarExtraView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -298,7 +324,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E180B6002992CD9300425DB0 /* KlipperMonTests.swift in Sources */,
E180B6002992CD9300425DB0 /* SoyuzTests.swift in Sources */,
E16378B429A491E6002F05E9 /* MoonrakerSocketManagerTests.swift in Sources */,
E1A93C6929CD627100BAE750 /* BonjourBrowserTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -306,8 +334,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E180B60A2992CD9300425DB0 /* KlipperMonUITests.swift in Sources */,
E180B60C2992CD9300425DB0 /* KlipperMonUITestsLaunchTests.swift in Sources */,
E180B60A2992CD9300425DB0 /* SoyuzUITests.swift in Sources */,
E180B60C2992CD9300425DB0 /* SoyuzUITestsLaunchTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -316,12 +344,12 @@
/* Begin PBXTargetDependency section */
E180B5FD2992CD9300425DB0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = E180B5E42992CD9100425DB0 /* KlipperMon */;
target = E180B5E42992CD9100425DB0 /* Soyuz */;
targetProxy = E180B5FC2992CD9300425DB0 /* PBXContainerItemProxy */;
};
E180B6072992CD9300425DB0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = E180B5E42992CD9100425DB0 /* KlipperMon */;
target = E180B5E42992CD9100425DB0 /* Soyuz */;
targetProxy = E180B6062992CD9300425DB0 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
@ -445,23 +473,25 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = KlipperMon/KlipperMon.entitlements;
CODE_SIGN_ENTITLEMENTS = Soyuz/Soyuz.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"KlipperMon/Preview Content\"";
DEVELOPMENT_ASSET_PATHS = "Soyuz/Preview\\ Content";
DEVELOPMENT_TEAM = W9ASV855X5;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = KlipperMon/Info.plist;
INFOPLIST_FILE = Soyuz/Info.plist;
INFOPLIST_KEY_LSUIElement = NO;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = info.maddie.KlipperMon;
PRODUCT_BUNDLE_IDENTIFIER = info.maddie.Soyuz;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -473,23 +503,25 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = KlipperMon/KlipperMon.entitlements;
CODE_SIGN_ENTITLEMENTS = Soyuz/Soyuz.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"KlipperMon/Preview Content\"";
DEVELOPMENT_ASSET_PATHS = "Soyuz/Preview\\ Content";
DEVELOPMENT_TEAM = W9ASV855X5;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = KlipperMon/Info.plist;
INFOPLIST_FILE = Soyuz/Info.plist;
INFOPLIST_KEY_LSUIElement = NO;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = info.maddie.KlipperMon;
PRODUCT_BUNDLE_IDENTIFIER = info.maddie.Soyuz;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@ -511,7 +543,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KlipperMon.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/KlipperMon";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Soyuz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Soyuz";
};
name = Debug;
};
@ -530,7 +562,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KlipperMon.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/KlipperMon";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Soyuz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Soyuz";
};
name = Release;
};
@ -571,7 +603,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
E180B5E02992CD9100425DB0 /* Build configuration list for PBXProject "KlipperMon" */ = {
E180B5E02992CD9100425DB0 /* Build configuration list for PBXProject "Soyuz" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E180B60D2992CD9300425DB0 /* Debug */,
@ -580,7 +612,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E180B60F2992CD9300425DB0 /* Build configuration list for PBXNativeTarget "KlipperMon" */ = {
E180B60F2992CD9300425DB0 /* Build configuration list for PBXNativeTarget "Soyuz" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E180B6102992CD9300425DB0 /* Debug */,
@ -589,7 +621,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E180B6122992CD9300425DB0 /* Build configuration list for PBXNativeTarget "KlipperMonTests" */ = {
E180B6122992CD9300425DB0 /* Build configuration list for PBXNativeTarget "SoyuzTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E180B6132992CD9300425DB0 /* Debug */,
@ -598,7 +630,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E180B6152992CD9300425DB0 /* Build configuration list for PBXNativeTarget "KlipperMonUITests" */ = {
E180B6152992CD9300425DB0 /* Build configuration list for PBXNativeTarget "SoyuzUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E180B6162992CD9300425DB0 /* Debug */,

View File

@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "starscream",
"kind" : "remoteSourceControl",
"location" : "https://github.com/daltoniam/Starscream.git",
"state" : {
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
"version" : "4.0.4"
}
}
],
"version" : 2
}

View File

@ -4,7 +4,7 @@
<dict>
<key>SchemeUserState</key>
<dict>
<key>KlipperMon.xcscheme_^#shared#^_</key>
<key>Soyuz.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "9DCD6317-B85A-47F1-8DA6-BE708C290036"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "1FA57A18-B3EC-4819-A5C7-77D0EA7B1B3C"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "SoyuzTests/BonjourBrowserTests.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "33"
endingLineNumber = "33"
landmarkName = "testBonjourDiscoveredItemsNotNil()"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "E361E24C-A6D3-4C02-B11D-290874A15033"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "SoyuzTests/MoonrakerSocketManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "34"
endingLineNumber = "34"
landmarkName = "testBlah()"
landmarkType = "7">
<Locations>
<Location
uuid = "E361E24C-A6D3-4C02-B11D-290874A15033 - 16ea1fa406b6a8b5"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "SoyuzTests.PrinterRequestManagerTests.testBlah() -&gt; ()"
moduleName = "SoyuzTests"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Volumes/SNAP/Users/averyadapace/Code/MacOS/KlipperMon/SoyuzTests/MoonrakerSocketManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "34"
endingLineNumber = "34"
offsetFromSymbolStart = "404">
</Location>
<Location
uuid = "E361E24C-A6D3-4C02-B11D-290874A15033 - 16ea1fa406b6a8b5"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "SoyuzTests.PrinterRequestManagerTests.testBlah() -&gt; ()"
moduleName = "SoyuzTests"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Volumes/SNAP/Users/averyadapace/Code/MacOS/KlipperMon/SoyuzTests/MoonrakerSocketManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "34"
endingLineNumber = "34"
offsetFromSymbolStart = "820">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -0,0 +1,19 @@
<?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>SchemeUserState</key>
<dict>
<key>KlipperMon.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>Soyuz.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,347 @@
{
"images" : [
{
"filename" : "40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "50.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "50x50"
},
{
"filename" : "100.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "50x50"
},
{
"filename" : "72.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "72x72"
},
{
"filename" : "144.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "72x72"
},
{
"filename" : "76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
},
{
"filename" : "48.png",
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "24x24",
"subtype" : "38mm"
},
{
"filename" : "55.png",
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "27.5x27.5",
"subtype" : "42mm"
},
{
"filename" : "58.png",
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "66.png",
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "33x33",
"subtype" : "45mm"
},
{
"filename" : "80.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "40x40",
"subtype" : "38mm"
},
{
"filename" : "88.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "44x44",
"subtype" : "40mm"
},
{
"filename" : "92.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "46x46",
"subtype" : "41mm"
},
{
"filename" : "100.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "50x50",
"subtype" : "44mm"
},
{
"filename" : "102.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "51x51",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "54x54",
"subtype" : "49mm"
},
{
"filename" : "172.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "86x86",
"subtype" : "38mm"
},
{
"filename" : "196.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "98x98",
"subtype" : "42mm"
},
{
"filename" : "216.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "108x108",
"subtype" : "44mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "117x117",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "129x129",
"subtype" : "49mm"
},
{
"filename" : "1024.png",
"idiom" : "watch-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.300",
"green" : "0.300",
"red" : "0.300"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

20
KlipperMon/Info.plist → Soyuz/Info.plist Normal file → Executable file
View File

@ -2,11 +2,13 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSBonjourServices</key>
<array>
<string>_moonraker._tcp.</string>
<string>_http._tcp.</string>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
@ -20,7 +22,11 @@
</array>
</dict>
</array>
<key>NSServices</key>
<array/>
<key>NSBonjourServices</key>
<array>
<string>_xctest._tcp.</string>
<string>_moonraker._tcp.</string>
<string>_http._tcp.</string>
</array>
</dict>
</plist>

View File

View File

@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

37
Soyuz/SoyuzApp.swift Executable file
View File

@ -0,0 +1,37 @@
//
// 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"
@ObservedObject static var printerManager = MoonrakerSocketManager()
var body: some Scene {
// WindowGroup(id: "floating-stats") {
// KlipperMonMenuBarExtraView(currentMenuBarIcon: $currentIcon)
// .environment(\.managedObjectContext, persistenceController.container.viewContext)
// }
WindowGroup("Configuration", id: "soyuz_cfg", content: {
PrinterConfigView(printerManager: SoyuzApp.printerManager)
//.frame(minWidth: 300, maxWidth: 600, minHeight: 60, maxHeight: 100)
})
.windowResizability(.contentSize)
MenuBarExtra("Soyuz", systemImage: currentIcon) {
SoyuzMenuBarExtraView(printerManager: SoyuzApp.printerManager, currentMenuBarIcon: $currentIcon)
.padding([.top, .leading, .trailing], 8)
.padding([.bottom], 6)
}
.menuBarExtraStyle(.window)
}
}

View File

@ -0,0 +1,75 @@
//
// BonjourBrowser.swift
// Soyuz
//
// Created by maddiefuzz on 3/20/23.
//
import Foundation
import Network
// Protocol defining minimal API for network discovery
// MARK: Net Discovery Protocol
protocol NetworkDiscoveryEngine {
func startScan(queue: DispatchQueue)
func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void))
func setStateUpdateHandler(_ handler: @escaping ((NWBrowser.State) -> Void))
}
extension NWBrowser: NetworkDiscoveryEngine {
func startScan(queue: DispatchQueue) {
start(queue: queue)
}
func setBrowseResultsChangedHandler(_ handler: @escaping ((Set<NWBrowser.Result>, Set<NWBrowser.Result.Change>) -> Void)) {
self.browseResultsChangedHandler = handler
}
func setStateUpdateHandler(_ handler: @escaping ((State) -> Void)) {
self.stateUpdateHandler = handler
}
}
// MARK: BonjourBrowser
class BonjourBrowser: ObservableObject {
@Published var NDEngineResults: [NWBrowser.Result] = []
private let nwBrowser: NetworkDiscoveryEngine
var connection: NWConnection!
// TEMPORARY
// var bonjourListener: NWListener?
init(browser: NetworkDiscoveryEngine = NWBrowser(for: .bonjourWithTXTRecord(type: "_moonraker._tcp", domain: "local."), using: .tcp)) {
nwBrowser = browser
// Bonjour browser results changed handler
nwBrowser.setBrowseResultsChangedHandler({ (newResults, changes) in
print("[update] Results changed.")
self.NDEngineResults.removeAll()
newResults.forEach { result in
print(result)
self.NDEngineResults.append(result)
}
})
// Bonjour browser state update handler
nwBrowser.setStateUpdateHandler({ 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.startScan(queue: DispatchQueue.main)
}
}

View File

@ -0,0 +1,242 @@
//
// MoonrakerSocketManager.swift
// KlipperMon
//
// Created by maddiefuzz on 2/7/23.
//
import Foundation
import Network
import AppKit
import Starscream
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()
private var starscreamEngine: Engine
// MARK: PRM init()
init(starscreamEngine: Engine = WSEngine(transport: TCPTransport())) {
state = ""
progress = 0.0
extruderTemperature = 0.0
bedTemperature = 0.0
socketHost = ""
socketPort = ""
self.starscreamEngine = starscreamEngine
// 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, engine: starscreamEngine)
socket!.delegate = self
print("About to connect to WebSocket at: \(request.debugDescription)")
socket!.connect()
}
private func reconnectWebsocket() {
if socket == nil {
return
}
socket!.disconnect()
self.openWebsocket()
}
// MARK: Callbacks
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)
}
}

View File

@ -0,0 +1,79 @@
//
// 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)
}
}
// 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?
}

View File

@ -0,0 +1,71 @@
//
// PrinterConfigView.swift
// KlipperMon
//
// Created by maddiefuzz on 2/8/23.
//
import SwiftUI
import Network
// MARK: PrinterConfigView
struct PrinterConfigView: View {
@ObservedObject var printerManager: MoonrakerSocketManager
@ObservedObject var bonjourBrowser = BonjourBrowser()
var body: some View {
VStack {
if(printerManager.isConnected) {
HStack {
Image(systemName: "network")
Text(printerManager.connection?.endpoint.toFriendlyString() ?? "Unknown Host")
Text("\(printerManager.socketHost):\(printerManager.socketPort)")
Button {
printerManager.disconnect()
} label: {
Text("Disconnect")
}
}
.frame(width: 500, height: 80)
} else {
VStack {
Text("Auto-detected Printers")
.font(.title)
ForEach(bonjourBrowser.NDEngineResults , id: \.hashValue) { result in
HStack {
Text(result.endpoint.toFriendlyString())
Button {
printerManager.connectToBonjourEndpoint(result.endpoint)
} label: {
Text("Connect")
//.foregroundColor(.white)
.padding()
}
}
}
}
.frame(width: 500, height: 100)
}
}
.onAppear {
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
}
struct PrinterConfigView_Previews: PreviewProvider {
@State static var printerManager = MoonrakerSocketManager()
static var previews: some View {
PrinterConfigView(printerManager: printerManager)
}
}
extension NWEndpoint {
func toFriendlyString() -> String {
let regex = /\.(.+)/
let match = self.debugDescription.firstMatch(of: regex)
return self.debugDescription.replacingOccurrences(of: match!.0, with: "")
}
}

View File

@ -0,0 +1,110 @@
//
// 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.
// Measured in degrees Celsius
let DANGERTEMP = 40.0
@Environment(\.openWindow) var openWindow
@ObservedObject var printerManager: MoonrakerSocketManager
@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(Color("ButtonForegroundColor"))
}
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"
@State static var printerManager = MoonrakerSocketManager()
static var previews: some View {
SoyuzMenuBarExtraView(printerManager: printerManager, currentMenuBarIcon: $currentMenuBarIcon)
}
}

View File

@ -0,0 +1,54 @@
//
// BonjourBrowserTests.swift
// SoyuzTests
//
// Created by maddiefuzz on 3/24/23.
//
import XCTest
import Network
import Combine
@testable import Soyuz
class SoyuzBonjourBrowserTests: XCTestCase {
var bonjourBrowser: BonjourBrowser?
var bonjourListener: NWListener?
var cancellable: AnyCancellable?
override func setUp() {
do {
bonjourListener = try NWListener(using: .tcp, on: .http)
bonjourListener!.service = NWListener.Service(name: "Test", type: "_xctest._tcp")
bonjourListener!.newConnectionHandler = { newConnection in
return
}
} catch {
print("Error: \(error)")
}
bonjourBrowser = BonjourBrowser(browser: NWBrowser(for: .bonjour(type: "_xctest._tcp", domain: "local."), using: .tcp))
}
func testBonjourDiscoveredItemsPopulated() {
guard let browser = bonjourBrowser else {
XCTAssert(false)
return
}
let expectation = XCTestExpectation(description: "BonjourBrowser publishes network services")
cancellable = browser.$NDEngineResults
.dropFirst()
.sink(receiveValue: { newValue in
if newValue.count > 0 {
expectation.fulfill()
}
})
bonjourListener!.start(queue: DispatchQueue.main)
wait(for: [expectation], timeout: 2)
XCTAssert(!browser.NDEngineResults.isEmpty)
XCTAssertEqual(browser.NDEngineResults.count, 1)
}
}

View File

@ -0,0 +1,130 @@
//
// MoonrakerSocketManagerTests.swift
// SoyuzTests
//
// Created by maddiefuzz on 2/21/23.
//
import XCTest
import Starscream
import Combine
import Network
@testable import Soyuz
class DummyEngine: Engine {
var delegate: Starscream.EngineDelegate?
@Published var startCalled = false
@Published var stopCalled = false
func resetForNextTest() {
self.startCalled = false
self.stopCalled = false
}
func register(delegate: Starscream.EngineDelegate) {
self.delegate = delegate
}
func start(request: URLRequest) {
startCalled = true
}
func stop(closeCode: UInt16) {
stopCalled = true
return
}
func forceStop() {
stopCalled = true
return
}
func write(data: Data, opcode: Starscream.FrameOpCode, completion: (() -> ())?) {
return
}
func write(string: String, completion: (() -> ())?) {
return
}
}
class MoonrakerSocketManagerTests: XCTestCase {
var socketManager: MoonrakerSocketManager?
var bonjourListener: NWListener?
var engine: DummyEngine!
var cancellable: AnyCancellable?
override func setUp() {
engine = DummyEngine()
socketManager = MoonrakerSocketManager(starscreamEngine: engine!)
do {
bonjourListener = try NWListener(using: .tcp, on: .http)
bonjourListener!.newConnectionHandler = { newConnection in
return
}
bonjourListener!.start(queue: DispatchQueue.main)
} catch {
print("Error: \(error)")
}
}
func testConnectToBonjourEndpoint() {
let endpoint = NWEndpoint.hostPort(host: "localhost", port: .http)
print("Trying to connect to bonjour endpoint \(endpoint)")
// Test connecting to endpoint
let connectExpectation = XCTestExpectation(description: "MoonrakerSocketManager.connectToBonjourEndpoint opens Starscream socket")
cancellable = engine.$startCalled
.sink(receiveValue: { newValue in
if newValue == true {
connectExpectation.fulfill()
}
})
socketManager?.connectToBonjourEndpoint(endpoint)
wait(for: [connectExpectation], timeout: 2)
XCTAssertTrue(engine.startCalled)
// Test screen sleeping
engine.resetForNextTest()
let screenSleepExpectation = XCTestExpectation(description: "MoonrakerSocketManager.screenChangedSleepState screen sleep triggers Starscream socket disconnection")
let sleepNotification = Notification(name: NSWorkspace.screensDidSleepNotification)
cancellable = engine.$stopCalled
.sink(receiveValue: { newValue in
if newValue == true {
screenSleepExpectation.fulfill()
}
})
socketManager?.screenChangedSleepState(sleepNotification)
wait(for: [screenSleepExpectation], timeout: 2)
XCTAssertTrue(engine.stopCalled)
// Test screen waking
engine.resetForNextTest()
let screenWakeExpectation = XCTestExpectation(description: "MoonrakerSocketManager.screenChangedSleepState screen wake triggers Starscream socket reconnection")
let wakeNotification = Notification(name: NSWorkspace.screensDidWakeNotification)
cancellable = engine.$startCalled
.sink(receiveValue: { newValue in
if newValue == true {
screenWakeExpectation.fulfill()
}
})
socketManager?.screenChangedSleepState(wakeNotification)
wait(for: [screenWakeExpectation], timeout: 2)
XCTAssertTrue(engine.startCalled)
}
}

36
SoyuzTests/SoyuzTests.swift Executable file
View File

@ -0,0 +1,36 @@
//
// KlipperMonTests.swift
// KlipperMonTests
//
// Created by maddiefuzz on 2/7/23.
//
import XCTest
@testable import Soyuz
final class SoyuzTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
// func testExample() throws {
// // This is an example of a functional test case.
// // Use XCTAssert and related functions to verify your tests produce the correct results.
// // Any test you write for XCTest can be annotated as throws and async.
// // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
// }
//
// func testPerformanceExample() throws {
// // This is an example of a performance test case.
// self.measure {
// // Put the code you want to measure the time of here.
// }
// }
}

View File

@ -22,20 +22,20 @@ final class KlipperMonUITests: XCTestCase {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
// func testExample() throws {
// // UI tests must launch the application that they test.
// let app = XCUIApplication()
// app.launch()
//
// // Use XCTAssert and related functions to verify your tests produce the correct results.
// }
//
// func testLaunchPerformance() throws {
// if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// // This measures how long it takes to launch your application.
// measure(metrics: [XCTApplicationLaunchMetric()]) {
// XCUIApplication().launch()
// }
// }
// }
}

View File

@ -0,0 +1,32 @@
//
// KlipperMonUITestsLaunchTests.swift
// KlipperMonUITests
//
// Created by maddiefuzz on 2/7/23.
//
import XCTest
final class KlipperMonUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
// func testLaunch() throws {
// let app = XCUIApplication()
// app.launch()
//
// // Insert steps here to perform after app launch but before taking a screenshot,
// // such as logging into a test account or navigating somewhere in the app
//
// let attachment = XCTAttachment(screenshot: app.screenshot())
// attachment.name = "Launch Screen"
// attachment.lifetime = .keepAlways
// add(attachment)
// }
}