Browse Source

add moonraker api backend

Thomas Buck 1 year ago
parent
commit
2753eb122b
6 changed files with 856 additions and 203 deletions
  1. 1
    1
      README.md
  2. 468
    0
      src/APIMoonraker.py
  3. 50
    22
      src/APIOctoprint.py
  4. 29
    22
      src/CamWindow.py
  5. 47
    25
      src/OctoTray.py
  6. 261
    133
      src/SettingsWindow.py

+ 1
- 1
README.md View File

1
 # OctoTray Linux Qt client
1
 # OctoTray Linux Qt client
2
 
2
 
3
 This is a simple Qt application living in the system tray.
3
 This is a simple Qt application living in the system tray.
4
-It allows remote-control and observation of 3D printers running OctoPrint.
4
+It allows remote-control and observation of 3D printers running OctoPrint or Moonraker.
5
 For the implementation it is using PyQt5.
5
 For the implementation it is using PyQt5.
6
 Automatic builds are provided for Linux, Windows and macOS.
6
 Automatic builds are provided for Linux, Windows and macOS.
7
 
7
 

+ 468
- 0
src/APIMoonraker.py View File

1
+#!/usr/bin/env python3
2
+
3
+# OctoTray Linux Qt System Tray OctoPrint client
4
+#
5
+# APIMoonraker.py
6
+#
7
+# HTTP API for Moonraker.
8
+
9
+import json
10
+import time
11
+import urllib.parse
12
+import urllib.request
13
+import operator
14
+import socket
15
+
16
+class APIMoonraker():
17
+    # TODO are these states correct?
18
+    statesWithWarning = [
19
+        "printing", "pausing", "paused"
20
+    ]
21
+
22
+    def __init__(self, parent, host, webcam):
23
+        self.parent = parent
24
+        self.host = host
25
+        self.webcamIndex = webcam
26
+
27
+    # return list of tuples ( "name", func(name) )
28
+    # with all available commands.
29
+    # call function with name of action!
30
+    def getAvailableCommands(self):
31
+        commands = []
32
+        self.devices = self.getDeviceList()
33
+
34
+        for d in self.devices:
35
+            #for a in [ "Turn on", "Turn off", "Toggle" ]:
36
+            for a in [ "Turn on", "Turn off" ]:
37
+                name = a + " " + d
38
+                cmd = ( name, self.toggleDevice )
39
+                commands.append(cmd)
40
+
41
+        return commands
42
+
43
+    ############
44
+    # HTTP API #
45
+    ############
46
+
47
+    # only used internally
48
+    def sendRequest(self, headers, path, content = None):
49
+        url = "http://" + self.host + "/" + path
50
+        if content == None:
51
+            request = urllib.request.Request(url, None, headers)
52
+        else:
53
+            data = content.encode('ascii')
54
+            request = urllib.request.Request(url, data, headers)
55
+
56
+        try:
57
+            with urllib.request.urlopen(request, None, self.parent.networkTimeout) as response:
58
+                text = response.read()
59
+                #print("Klipper Rx: \"" + str(text) + "\"\n")
60
+                return text
61
+        except (urllib.error.URLError, urllib.error.HTTPError) as error:
62
+            print("Error requesting URL \"" + url + "\": \"" + str(error) + "\"")
63
+            return "error"
64
+        except socket.timeout:
65
+            print("Timeout waiting for response to \"" + url + "\"")
66
+            return "timeout"
67
+
68
+    # only used internally
69
+    def sendPostRequest(self, path, content):
70
+        headers = {
71
+            "Content-Type": "application/json"
72
+        }
73
+        return self.sendRequest(headers, path, content)
74
+
75
+    # only used internally
76
+    def sendGetRequest(self, path):
77
+        headers = {}
78
+        return self.sendRequest(headers, path)
79
+
80
+    #####################
81
+    # Command discovery #
82
+    #####################
83
+
84
+    def getDeviceList(self):
85
+        devices = []
86
+
87
+        r = self.sendGetRequest("machine/device_power/devices")
88
+        if (r == "timeout") or (r == "error"):
89
+            return devices
90
+
91
+        try:
92
+            rd = json.loads(r)
93
+            if "result" in rd:
94
+                if "devices" in rd["result"]:
95
+                    for d in rd["result"]["devices"]:
96
+                        if "device" in d:
97
+                            devices.append(d["device"])
98
+
99
+        except json.JSONDecodeError:
100
+            pass
101
+
102
+        return devices
103
+
104
+    # return "unknown" when no power can be toggled
105
+    def getMethod(self):
106
+        if len(self.devices) <= 0:
107
+            return "unknown"
108
+        return "moonraker"
109
+
110
+    #################
111
+    # Safety Checks #
112
+    #################
113
+
114
+    # only used internally
115
+    def stateSafetyCheck(self, actionString):
116
+        state = self.getState()
117
+        if state.lower() in self.statesWithWarning:
118
+            if self.parent.showDialog("OctoTray Warning", "The printer seems to be running currently!", "Do you really want to " + actionString + "?", True, True) == False:
119
+                return True
120
+        return False
121
+
122
+    # only used internally
123
+    def tempSafetyCheck(self, actionString):
124
+        if self.getTemperatureIsSafe() == False:
125
+            if self.parent.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to " + actionString + "?", True, True) == False:
126
+                return True
127
+        return False
128
+
129
+    # only used internally
130
+    def safetyCheck(self, actionString):
131
+        if self.stateSafetyCheck(actionString):
132
+            return True
133
+        if self.tempSafetyCheck(actionString):
134
+            return True
135
+        return False
136
+
137
+    ##################
138
+    # Power Toggling #
139
+    ##################
140
+
141
+    # only used internally (passed to caller as a pointer)
142
+    def toggleDevice(self, name):
143
+        # name is "Toggle x" or "Turn on x" or "Turn off x"
144
+        action = ""
145
+        if name.startswith("Toggle "):
146
+            action = "toggle"
147
+            name = name[len("Toggle "):]
148
+        elif name.startswith("Turn on "):
149
+            action = "on"
150
+            name = name[len("Turn on "):]
151
+        elif name.startswith("Turn off "):
152
+            action = "off"
153
+            name = name[len("Turn off "):]
154
+
155
+        self.sendPostRequest("machine/device_power/device?device=" + name + "&action=" + action, "")
156
+
157
+    # should automatically turn on printer, regardless of method
158
+    def turnOn(self):
159
+        if len(self.devices) > 0:
160
+            self.toggleDevice("Turn on " + self.devices[0])
161
+
162
+    # should automatically turn off printer, regardless of method
163
+    def turnOff(self):
164
+        if len(self.devices) > 0:
165
+            self.toggleDevice("Turn off " + self.devices[0])
166
+
167
+    ######################
168
+    # Status Information #
169
+    ######################
170
+
171
+    # only used internally
172
+    def getState(self):
173
+        # just using octoprint compatibility layer
174
+        r = self.sendGetRequest("api/job")
175
+        try:
176
+            rd = json.loads(r)
177
+            if "state" in rd:
178
+                return rd["state"]
179
+        except json.JSONDecodeError:
180
+            pass
181
+        return "Unknown"
182
+
183
+    # only used internally
184
+    def getTemperatureIsSafe(self, limit = 50.0):
185
+        self.sendGetRequest("printer/objects/query?extruder=temperature")
186
+        if (r == "timeout") or (r == "error"):
187
+            return files
188
+
189
+        temp = 0.0
190
+
191
+        try:
192
+            rd = json.loads(r)
193
+            if "result" in rd:
194
+                if "status" in rd["result"]:
195
+                    if "extruder" in rd["result"]["status"]:
196
+                        if "temperature" in rd["result"]["status"]["extruder"]:
197
+                            temp = float(rd["result"]["status"]["extruder"]["temperature"])
198
+
199
+        except json.JSONDecodeError:
200
+            pass
201
+
202
+        return temp < limit
203
+
204
+    # human readable temperatures
205
+    def getTemperatureString(self):
206
+        r = self.sendGetRequest("printer/objects/query?extruder=temperature,target")
207
+        s = "Unknown"
208
+
209
+        try:
210
+            rd = json.loads(r)
211
+            if "result" in rd:
212
+                if "status" in rd["result"]:
213
+                    if "extruder" in rd["result"]["status"]:
214
+                        temp = 0.0
215
+                        target = 0.0
216
+                        if "temperature" in rd["result"]["status"]["extruder"]:
217
+                            temp = float(rd["result"]["status"]["extruder"]["temperature"])
218
+                        if "target" in rd["result"]["status"]["extruder"]:
219
+                            target = float(rd["result"]["status"]["extruder"]["target"])
220
+                        s = str(temp) + " / " + str(target)
221
+
222
+        except json.JSONDecodeError:
223
+            pass
224
+
225
+        return s
226
+
227
+    # human readable name (fall back to hostname)
228
+    def getName(self):
229
+        r = self.sendGetRequest("printer/info")
230
+        s = self.host
231
+
232
+        try:
233
+            rd = json.loads(r)
234
+            if "result" in rd:
235
+                if "hostname" in rd["result"]:
236
+                    s = rd["result"]["hostname"]
237
+
238
+        except json.JSONDecodeError:
239
+            pass
240
+
241
+        return s
242
+
243
+    # only used internally
244
+    def getProgress(self):
245
+        # just using octoprint compatibility layer
246
+        r = self.sendGetRequest("api/job")
247
+        try:
248
+            rd = json.loads(r)
249
+            if "progress" in rd:
250
+                return rd["progress"]
251
+        except json.JSONDecodeError:
252
+            pass
253
+        return "Unknown"
254
+
255
+    # human readable progress
256
+    def getProgressString(self):
257
+        # just using octoprint compatibility layer
258
+        s = ""
259
+        progress = self.getProgress()
260
+        if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
261
+            s += "%.1f%%" % progress["completion"]
262
+            s += " - runtime "
263
+            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTime"]))
264
+            s += " - "
265
+            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
266
+        return s
267
+
268
+    ###################
269
+    # Printer Actions #
270
+    ###################
271
+
272
+    # only used internally
273
+    def sendGCode(self, cmd):
274
+        self.sendPostRequest("printer/gcode/script?script=" + cmd, "")
275
+
276
+    # only used internally
277
+    def isPaused(self):
278
+        r = self.sendGetRequest("objects/query?pause_resume")
279
+
280
+        p = False
281
+
282
+        try:
283
+            rd = json.loads(r)
284
+            if "result" in rd:
285
+                if "status" in rd["result"]:
286
+                    if "pause_resume" in rd["result"]["status"]:
287
+                        if "is_paused" in rd["result"]["status"]["pause_resume"]:
288
+                            p = rd["result"]["status"]["pause_resume"]["is_paused"]
289
+
290
+        except json.JSONDecodeError:
291
+            pass
292
+
293
+        return bool(p)
294
+
295
+    # only used internally
296
+    def isPositioningAbsolute(self):
297
+        r = self.sendGetRequest("printer/objects/query?gcode_move=absolute_coordinates")
298
+
299
+        p = True
300
+
301
+        try:
302
+            rd = json.loads(r)
303
+            if "result" in rd:
304
+                if "status" in rd["result"]:
305
+                    if "gcode_move" in rd["result"]["status"]:
306
+                        if "absolute_coordinates" in rd["result"]["status"]["gcode_move"]:
307
+                            p = rd["result"]["status"]["gcode_move"]["absolute_coordinates"]
308
+
309
+        except json.JSONDecodeError:
310
+            pass
311
+
312
+        return bool(p)
313
+
314
+    def callHoming(self, axes = "xyz"):
315
+        if self.stateSafetyCheck("home it"):
316
+            return
317
+
318
+        # always home in XYZ order
319
+        if "x" in axes:
320
+            self.sendGCode("G28 X")
321
+        if "y" in axes:
322
+            self.sendGCode("G28 Y")
323
+        if "z" in axes:
324
+            self.sendGCode("G28 Z")
325
+
326
+    def callMove(self, axis, dist, speed, relative = True):
327
+        if self.stateSafetyCheck("move it"):
328
+            return
329
+
330
+        currentlyAbsolute = self.isPositioningAbsolute()
331
+
332
+        if currentlyAbsolute and relative:
333
+            # set to relative positioning
334
+            self.sendGCode("G91")
335
+
336
+        if (not currentlyAbsolute) and (not relative):
337
+            # set to absolute positioning
338
+            self.sendGCode("G90")
339
+
340
+        # do move
341
+        if axis.lower() == "x":
342
+            self.sendGCode("G0 X" + str(dist) + " F" + str(speed))
343
+        elif axis.lower() == "y":
344
+            self.sendGCode("G0 Y" + str(dist) + " F" + str(speed))
345
+        elif axis.lower() == "z":
346
+            self.sendGCode("G0 Z" + str(dist) + " F" + str(speed))
347
+
348
+        if currentlyAbsolute and relative:
349
+            # set to absolute positioning
350
+            self.sendGCode("G90")
351
+
352
+        if (not currentlyAbsolute) and (not relative):
353
+            # set to relative positioning
354
+            self.sendGCode("G91")
355
+
356
+    def callPauseResume(self):
357
+        if self.stateSafetyCheck("pause/resume"):
358
+            return
359
+
360
+        if self.isPaused():
361
+            self.sendPostRequest("printer/print/pause", "")
362
+        else:
363
+            self.sendPostRequest("printer/print/resume", "")
364
+
365
+    def callJobCancel(self):
366
+        if self.stateSafetyCheck("cancel"):
367
+            return
368
+
369
+        self.sendPostRequest("printer/print/cancel")
370
+
371
+    def statusDialog(self):
372
+        progress = self.getProgress()
373
+        s = self.getName() + "\n"
374
+        warning = False
375
+        if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
376
+            s += "%.1f%% Completion\n" % progress["completion"]
377
+            s += "Printing since " + time.strftime("%H:%M:%S", time.gmtime(progress["printTime"])) + "\n"
378
+            s += time.strftime("%H:%M:%S", time.gmtime(progress["printTimeLeft"])) + " left"
379
+        elif ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress):
380
+            s += "No job is currently running"
381
+        else:
382
+            s += "Could not read printer status!"
383
+            warning = True
384
+        t = self.getTemperatureString()
385
+        if len(t) > 0:
386
+            s += "\n" + t
387
+        self.parent.showDialog("OctoTray Status", s, None, False, warning)
388
+
389
+    #################
390
+    # File Handling #
391
+    #################
392
+
393
+    def getRecentFiles(self, count):
394
+        files = []
395
+
396
+        r = self.sendGetRequest("server/files/directory")
397
+        if (r == "timeout") or (r == "error"):
398
+            return files
399
+
400
+        try:
401
+            rd = json.loads(r)
402
+            if "result" in rd:
403
+                if "files" in rd["result"]:
404
+                    for f in rd["result"]["files"]:
405
+                        if "filename" in f and "modified" in f:
406
+                            tmp = (f["filename"], f["modified"])
407
+                            files.append(tmp)
408
+
409
+        except json.JSONDecodeError:
410
+            pass
411
+
412
+        files.sort(reverse = True, key = lambda x: x[1])
413
+        files = files[:count]
414
+        return [ ( i[0], i[0] ) for i in files ]
415
+
416
+    def printFile(self, path):
417
+        self.sendPostRequest("printer/print/start?filename=" + path, "")
418
+
419
+    ###############
420
+    # Temperature #
421
+    ###############
422
+
423
+    # only used internally
424
+    def setTemperature(self, cmd, temp):
425
+        cmd_str = cmd + " " + str(int(temp))
426
+        self.sendGCode(cmd_str)
427
+
428
+    def printerHeatTool(self, temp):
429
+        self.setTemperature("M104", temp)
430
+
431
+    def printerHeatBed(self, temp):
432
+        self.setTemperature("M140", temp)
433
+
434
+    def printerCooldown(self):
435
+        if self.stateSafetyCheck("cool it down"):
436
+            return
437
+
438
+        self.printerHeatTool(0)
439
+        self.printerHeatBed(0)
440
+
441
+    ##########
442
+    # Webcam #
443
+    ##########
444
+
445
+    def getWebcamURL(self):
446
+        url = ""
447
+
448
+        r = self.sendGetRequest("server/webcams/list")
449
+        if (r == "timeout") or (r == "error"):
450
+            return url
451
+
452
+        try:
453
+            rd = json.loads(r)
454
+            if "result" in rd:
455
+                if "webcams" in rd["result"]:
456
+                    if len(rd["result"]["webcams"]) > self.webcamIndex:
457
+                        w = rd["result"]["webcams"][self.webcamIndex]
458
+                        if "snapshot_url" in w:
459
+                            url =  w["snapshot_url"]
460
+
461
+        except json.JSONDecodeError:
462
+            pass
463
+
464
+        # make relative paths absolute
465
+        if url.startswith("/"):
466
+            url = "http://" + self.host + url
467
+
468
+        return url

+ 50
- 22
src/APIOctoprint.py View File

25
 
25
 
26
     # return list of tuples ( "name", func(name) )
26
     # return list of tuples ( "name", func(name) )
27
     # with all available commands.
27
     # with all available commands.
28
-    # call function in with name of action!
28
+    # call function with name of action!
29
     def getAvailableCommands(self):
29
     def getAvailableCommands(self):
30
-        self.method = self.getMethod()
31
-        print("Printer " + self.host + " has method " + self.method)
30
+        self.method = self.getMethodInternal()
31
+        print("OctoPrint " + self.host + " has method " + self.method)
32
 
32
 
33
         commands = []
33
         commands = []
34
 
34
 
52
     # HTTP API #
52
     # HTTP API #
53
     ############
53
     ############
54
 
54
 
55
+    # only used internally
55
     def sendRequest(self, headers, path, content = None):
56
     def sendRequest(self, headers, path, content = None):
56
         url = "http://" + self.host + "/api/" + path
57
         url = "http://" + self.host + "/api/" + path
57
         if content == None:
58
         if content == None:
71
             print("Timeout waiting for response to \"" + url + "\"")
72
             print("Timeout waiting for response to \"" + url + "\"")
72
             return "timeout"
73
             return "timeout"
73
 
74
 
75
+    # only used internally
74
     def sendPostRequest(self, path, content):
76
     def sendPostRequest(self, path, content):
75
         headers = {
77
         headers = {
76
             "Content-Type": "application/json",
78
             "Content-Type": "application/json",
78
         }
80
         }
79
         return self.sendRequest(headers, path, content)
81
         return self.sendRequest(headers, path, content)
80
 
82
 
83
+    # only used internally
81
     def sendGetRequest(self, path):
84
     def sendGetRequest(self, path):
82
         headers = {
85
         headers = {
83
             "X-Api-Key": self.key
86
             "X-Api-Key": self.key
88
     # Command discovery #
91
     # Command discovery #
89
     #####################
92
     #####################
90
 
93
 
91
-    def getMethod(self):
94
+    # only used internally
95
+    def getMethodInternal(self):
92
         r = self.sendGetRequest("plugin/psucontrol")
96
         r = self.sendGetRequest("plugin/psucontrol")
93
-        if r == "timeout":
94
-            return "unknown"
95
-
96
-        try:
97
-            rd = json.loads(r)
98
-            if "isPSUOn" in rd:
99
-                return "psucontrol"
100
-        except json.JSONDecodeError:
101
-            pass
97
+        if (r != "timeout") and (r != "error"):
98
+            try:
99
+                rd = json.loads(r)
100
+                if "isPSUOn" in rd:
101
+                    return "psucontrol"
102
+            except json.JSONDecodeError:
103
+                pass
102
 
104
 
103
         r = self.sendGetRequest("system/commands/custom")
105
         r = self.sendGetRequest("system/commands/custom")
104
-        if r == "timeout":
106
+        if (r == "timeout") or (r == "error"):
105
             return "unknown"
107
             return "unknown"
106
 
108
 
107
         try:
109
         try:
117
 
119
 
118
         return "unknown"
120
         return "unknown"
119
 
121
 
122
+    # return "unknown" when no power can be toggled
123
+    def getMethod(self):
124
+        return self.method
125
+
126
+    # only used internally
120
     def getSystemCommands(self):
127
     def getSystemCommands(self):
121
         l = []
128
         l = []
122
         r = self.sendGetRequest("system/commands/custom")
129
         r = self.sendGetRequest("system/commands/custom")
138
     # Safety Checks #
145
     # Safety Checks #
139
     #################
146
     #################
140
 
147
 
148
+    # only used internally
141
     def stateSafetyCheck(self, actionString):
149
     def stateSafetyCheck(self, actionString):
142
         state = self.getState()
150
         state = self.getState()
143
         if state.lower() in self.statesWithWarning:
151
         if state.lower() in self.statesWithWarning:
145
                 return True
153
                 return True
146
         return False
154
         return False
147
 
155
 
156
+    # only used internally
148
     def tempSafetyCheck(self, actionString):
157
     def tempSafetyCheck(self, actionString):
149
         if self.getTemperatureIsSafe() == False:
158
         if self.getTemperatureIsSafe() == False:
150
             if self.parent.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to " + actionString + "?", True, True) == False:
159
             if self.parent.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to " + actionString + "?", True, True) == False:
151
                 return True
160
                 return True
152
         return False
161
         return False
153
 
162
 
163
+    # only used internally
154
     def safetyCheck(self, actionString):
164
     def safetyCheck(self, actionString):
155
         if self.stateSafetyCheck(actionString):
165
         if self.stateSafetyCheck(actionString):
156
             return True
166
             return True
162
     # Power Toggling #
172
     # Power Toggling #
163
     ##################
173
     ##################
164
 
174
 
175
+    # only used internally (passed to caller as a pointer)
165
     def callSystemCommand(self, name):
176
     def callSystemCommand(self, name):
166
         if "off" in name.lower():
177
         if "off" in name.lower():
167
             if self.safetyCheck("run '" + name + "'"):
178
             if self.safetyCheck("run '" + name + "'"):
170
         cmd = urllib.parse.quote(name)
181
         cmd = urllib.parse.quote(name)
171
         self.sendPostRequest("system/commands/custom/" + cmd, '')
182
         self.sendPostRequest("system/commands/custom/" + cmd, '')
172
 
183
 
184
+    # only used internally (passed to caller as a pointer)
173
     def setPower(self, name):
185
     def setPower(self, name):
174
         if "off" in name.lower():
186
         if "off" in name.lower():
175
             if self.safetyCheck(name):
187
             if self.safetyCheck(name):
181
 
193
 
182
         return self.sendPostRequest("plugin/psucontrol", '{ "command":"' + cmd + '" }')
194
         return self.sendPostRequest("plugin/psucontrol", '{ "command":"' + cmd + '" }')
183
 
195
 
196
+    # should automatically turn on printer, regardless of method
184
     def turnOn(self):
197
     def turnOn(self):
185
         if self.method == "psucontrol":
198
         if self.method == "psucontrol":
186
             self.setPower("on")
199
             self.setPower("on")
191
                     self.callSystemCommand(cmd)
204
                     self.callSystemCommand(cmd)
192
                     break
205
                     break
193
 
206
 
207
+    # should automatically turn off printer, regardless of method
194
     def turnOff(self):
208
     def turnOff(self):
195
         if self.method == "psucontrol":
209
         if self.method == "psucontrol":
196
             self.setPower("off")
210
             self.setPower("off")
205
     # Status Information #
219
     # Status Information #
206
     ######################
220
     ######################
207
 
221
 
222
+    # only used internally
208
     def getTemperatureIsSafe(self, limit = 50.0):
223
     def getTemperatureIsSafe(self, limit = 50.0):
209
         r = self.sendGetRequest("printer")
224
         r = self.sendGetRequest("printer")
210
         try:
225
         try:
222
             pass
237
             pass
223
         return True
238
         return True
224
 
239
 
240
+    # human readable temperatures
225
     def getTemperatureString(self):
241
     def getTemperatureString(self):
226
         r = self.sendGetRequest("printer")
242
         r = self.sendGetRequest("printer")
227
         s = ""
243
         s = ""
261
                 s += " "
277
                 s += " "
262
         return s.strip()
278
         return s.strip()
263
 
279
 
280
+    # only used internally
264
     def getState(self):
281
     def getState(self):
265
         r = self.sendGetRequest("job")
282
         r = self.sendGetRequest("job")
266
         try:
283
         try:
271
             pass
288
             pass
272
         return "Unknown"
289
         return "Unknown"
273
 
290
 
291
+    # only used internally
274
     def getProgress(self):
292
     def getProgress(self):
275
         r = self.sendGetRequest("job")
293
         r = self.sendGetRequest("job")
276
         try:
294
         try:
281
             pass
299
             pass
282
         return "Unknown"
300
         return "Unknown"
283
 
301
 
302
+    # human readable name (fall back to hostname)
284
     def getName(self):
303
     def getName(self):
285
         r = self.sendGetRequest("printerprofiles")
304
         r = self.sendGetRequest("printerprofiles")
286
         try:
305
         try:
293
             pass
312
             pass
294
         return self.host
313
         return self.host
295
 
314
 
315
+    # human readable progress
296
     def getProgressString(self):
316
     def getProgressString(self):
297
         s = ""
317
         s = ""
298
         progress = self.getProgress()
318
         progress = self.getProgress()
320
 
340
 
321
         self.sendPostRequest("printer/printhead", '{ "command": "home", "axes": [' + axes_string + '] }')
341
         self.sendPostRequest("printer/printhead", '{ "command": "home", "axes": [' + axes_string + '] }')
322
 
342
 
323
-    def callMove(self, axis, dist, relative = True):
343
+    def callMove(self, axis, dist, speed, relative = True):
324
         if self.stateSafetyCheck("move it"):
344
         if self.stateSafetyCheck("move it"):
325
             return
345
             return
326
 
346
 
328
         if relative == False:
348
         if relative == False:
329
             absolute = ', "absolute": true'
349
             absolute = ', "absolute": true'
330
 
350
 
331
-        self.sendPostRequest("printer/printhead", '{ "command": "jog", "' + str(axis) + '": ' + str(dist) + ', "speed": ' + str(self.jogMoveSpeed) + absolute + ' }')
351
+        self.sendPostRequest("printer/printhead", '{ "command": "jog", "' + str(axis) + '": ' + str(dist) + ', "speed": ' + str(speed) + absolute + ' }')
332
 
352
 
333
     def callPauseResume(self):
353
     def callPauseResume(self):
334
         if self.stateSafetyCheck("pause/resume"):
354
         if self.stateSafetyCheck("pause/resume"):
342
 
362
 
343
     def statusDialog(self):
363
     def statusDialog(self):
344
         progress = self.getProgress()
364
         progress = self.getProgress()
345
-        s = self.host + "\n"
365
+        s = self.getName() + "\n"
346
         warning = False
366
         warning = False
347
         if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
367
         if ("completion" in progress) and ("printTime" in progress) and ("printTimeLeft" in progress) and (progress["completion"] != None) and (progress["printTime"] != None) and (progress["printTimeLeft"] != None):
348
             s += "%.1f%% Completion\n" % progress["completion"]
368
             s += "%.1f%% Completion\n" % progress["completion"]
383
     # Temperature #
403
     # Temperature #
384
     ###############
404
     ###############
385
 
405
 
406
+    # only used internally
386
     def setTemperature(self, what, temp):
407
     def setTemperature(self, what, temp):
408
+        if temp == None:
409
+            temp = 0
410
+
387
         path = "printer/bed"
411
         path = "printer/bed"
388
-        s = "{\"command\": \"target\", \"target\": " + temp + "}"
412
+        s = "{\"command\": \"target\", \"target\": " + str(temp) + "}"
389
 
413
 
390
         if "tool" in what:
414
         if "tool" in what:
391
             path = "printer/tool"
415
             path = "printer/tool"
392
-            s = "{\"command\": \"target\", \"targets\": {\"" + what + "\": " + temp + "}}"
393
-
394
-        if temp == None:
395
-            temp = 0
416
+            s = "{\"command\": \"target\", \"targets\": {\"" + str(what) + "\": " + str(temp) + "}}"
396
 
417
 
397
         self.sendPostRequest(path, s)
418
         self.sendPostRequest(path, s)
398
 
419
 
408
 
429
 
409
         self.setTemperature("tool0", 0)
430
         self.setTemperature("tool0", 0)
410
         self.setTemperature("bed", 0)
431
         self.setTemperature("bed", 0)
432
+
433
+    ##########
434
+    # Webcam #
435
+    ##########
436
+
437
+    def getWebcamURL(self):
438
+        return "http://" + self.host + ":8080/?action=snapshot"

+ 29
- 22
src/CamWindow.py View File

28
         self.manager.finished.connect(self.handleResponse)
28
         self.manager.finished.connect(self.handleResponse)
29
         self.parent = parent
29
         self.parent = parent
30
         self.printer = printer
30
         self.printer = printer
31
-        self.url = "http://" + self.printer.host + ":8080/?action=snapshot"
31
+
32
+        self.url = self.printer.api.getWebcamURL()
33
+        print("Webcam: " + self.url)
32
 
34
 
33
         self.setWindowTitle(parent.name + " Webcam Stream")
35
         self.setWindowTitle(parent.name + " Webcam Stream")
34
         self.setWindowIcon(parent.icon)
36
         self.setWindowIcon(parent.icon)
163
         self.printer.api.callJobCancel()
165
         self.printer.api.callJobCancel()
164
 
166
 
165
     def moveXP(self):
167
     def moveXP(self):
166
-        self.printer.api.callMove("x", int(self.parent.jogMoveLength), True)
168
+        self.printer.api.callMove("x", int(self.printer.jogLength), int(self.printer.jogSpeed), True)
167
 
169
 
168
     def moveXM(self):
170
     def moveXM(self):
169
-        self.printer.api.callMove("x", -1 * int(self.parent.jogMoveLength), True)
171
+        self.printer.api.callMove("x", -1 * int(self.printer.jogLength), int(self.printer.jogSpeed), True)
170
 
172
 
171
     def moveYP(self):
173
     def moveYP(self):
172
-        self.printer.api.callMove("y", int(self.parent.jogMoveLength), True)
174
+        self.printer.api.callMove("y", int(self.printer.jogLength), int(self.printer.jogSpeed), True)
173
 
175
 
174
     def moveYM(self):
176
     def moveYM(self):
175
-        self.printer.api.callMove("y", -1 * int(self.parent.jogMoveLength), True)
177
+        self.printer.api.callMove("y", -1 * int(self.printer.jogLength), int(self.printer.jogSpeed), True)
176
 
178
 
177
     def moveZP(self):
179
     def moveZP(self):
178
-        self.printer.api.callMove("z", int(self.parent.jogMoveLength), True)
180
+        self.printer.api.callMove("z", int(self.printer.jogLength), int(self.printer.jogSpeed), True)
179
 
181
 
180
     def moveZM(self):
182
     def moveZM(self):
181
-        self.printer.api.callMove("z", -1 * int(self.parent.jogMoveLength), True)
183
+        self.printer.api.callMove("z", -1 * int(self.printer.jogLength), int(self.printer.jogSpeed), True)
182
 
184
 
183
     def homeX(self):
185
     def homeX(self):
184
         self.printer.api.callHoming("x")
186
         self.printer.api.callHoming("x")
199
         self.printer.api.turnOff()
201
         self.printer.api.turnOff()
200
 
202
 
201
     def cooldown(self):
203
     def cooldown(self):
202
-        self.printer.api.printerCooldown(self.printer)
204
+        self.printer.api.printerCooldown()
203
 
205
 
204
     def preheatTool(self):
206
     def preheatTool(self):
205
         self.printer.api.printerHeatTool(self.printer.tempTool)
207
         self.printer.api.printerHeatTool(self.printer.tempTool)
251
         self.scheduleLoadStatus()
253
         self.scheduleLoadStatus()
252
 
254
 
253
     def handleResponse(self, reply):
255
     def handleResponse(self, reply):
254
-        if reply.url().url() == self.url:
255
-            if reply.error() == QtNetwork.QNetworkReply.NoError:
256
-                reader = QImageReader(reply)
257
-                reader.setAutoTransform(True)
258
-                image = reader.read()
259
-                if image != None:
260
-                    if image.colorSpace().isValid():
261
-                        image.convertToColorSpace(QColorSpace.SRgb)
262
-                    self.img.setPixmap(QPixmap.fromImage(image))
263
-                    self.scheduleLoadImage()
264
-                else:
265
-                    print("Error decoding image: " + reader.errorString())
266
-            else:
267
-                print("Error loading image: " + reply.errorString())
256
+        if reply.url().url() != self.url:
257
+            print("Reponse for unknown resource: " + reply.url().url())
258
+            return
259
+
260
+        if reply.error() != QtNetwork.QNetworkReply.NoError:
261
+            print("Error loading image: " + reply.errorString())
262
+            return
263
+
264
+        reader = QImageReader(reply)
265
+        reader.setAutoTransform(True)
266
+        image = reader.read()
267
+        if image == None:
268
+            print("Error decoding image: " + reader.errorString())
269
+            return
270
+
271
+        if image.colorSpace().isValid():
272
+            image.convertToColorSpace(QColorSpace.SRgb)
273
+        self.img.setPixmap(QPixmap.fromImage(image))
274
+        self.scheduleLoadImage()

+ 47
- 25
src/OctoTray.py View File

14
 from PyQt5.QtCore import QCoreApplication, QSettings, QUrl
14
 from PyQt5.QtCore import QCoreApplication, QSettings, QUrl
15
 from CamWindow import CamWindow
15
 from CamWindow import CamWindow
16
 from SettingsWindow import SettingsWindow
16
 from SettingsWindow import SettingsWindow
17
+from SettingsWindow import Printer
17
 from MainWindow import MainWindow
18
 from MainWindow import MainWindow
18
 from APIOctoprint import APIOctoprint
19
 from APIOctoprint import APIOctoprint
19
-
20
-class Printer(object):
21
-    # field 'api' for actual I/O
22
-    # field 'host' and 'key' for credentials
23
-    pass
20
+from APIMoonraker import APIMoonraker
24
 
21
 
25
 class OctoTray():
22
 class OctoTray():
26
     name = "OctoTray"
23
     name = "OctoTray"
46
     settingsWindow = None
43
     settingsWindow = None
47
 
44
 
48
     # default, can be overridden in config
45
     # default, can be overridden in config
49
-    jogMoveSpeed = 10 * 60 # in mm/min
50
-    jogMoveLength = 10 # in mm
46
+    jogMoveSpeedDefault = 10 * 60 # in mm/min
47
+    jogMoveLengthDefault = 10 # in mm
51
 
48
 
52
     def __init__(self, app, inSysTray):
49
     def __init__(self, app, inSysTray):
53
         QCoreApplication.setApplicationName(self.name)
50
         QCoreApplication.setApplicationName(self.name)
60
 
57
 
61
         unknownCount = 0
58
         unknownCount = 0
62
         for p in self.printers:
59
         for p in self.printers:
63
-            p.api = APIOctoprint(self, p.host, p.key)
64
             p.menus = []
60
             p.menus = []
65
 
61
 
62
+            if p.apiType.lower() == "octoprint":
63
+                p.api = APIOctoprint(self, p.host, p.key)
64
+            elif p.apiType.lower() == "moonraker":
65
+                p.api = APIMoonraker(self, p.host, p.webcam)
66
+            else:
67
+                print("Unsupported API type " + p.apiType)
68
+                unknownCount += 1
69
+                action = QAction(p.host)
70
+                action.setEnabled(False)
71
+                p.menus.append(action)
72
+                self.menu.addAction(action)
73
+                continue
74
+
66
             commands = p.api.getAvailableCommands()
75
             commands = p.api.getAvailableCommands()
67
 
76
 
68
             # don't populate menu when no methods are available
77
             # don't populate menu when no methods are available
83
             for cmd in commands:
92
             for cmd in commands:
84
                 name, func = cmd
93
                 name, func = cmd
85
                 action = QAction(name)
94
                 action = QAction(name)
86
-                action.triggered.connect(lambda chk, p=p, n=name, f=func: p.api.f(n))
95
+                action.triggered.connect(lambda chk, n=name, f=func: f(n))
87
                 p.menus.append(action)
96
                 p.menus.append(action)
88
                 menu.addAction(action)
97
                 menu.addAction(action)
89
 
98
 
192
     def readSettings(self):
201
     def readSettings(self):
193
         settings = QSettings(self.vendor, self.name)
202
         settings = QSettings(self.vendor, self.name)
194
 
203
 
195
-        js = settings.value("jog_speed")
196
-        if js != None:
197
-            self.jogMoveSpeed = int(js)
198
-
199
-        jl = settings.value("jog_length")
200
-        if jl != None:
201
-            self.jogMoveLength = int(jl)
202
-
203
         printers = []
204
         printers = []
204
         l = settings.beginReadArray("printers")
205
         l = settings.beginReadArray("printers")
205
         for i in range(0, l):
206
         for i in range(0, l):
206
             settings.setArrayIndex(i)
207
             settings.setArrayIndex(i)
207
             p = Printer()
208
             p = Printer()
208
-            p.host = settings.value("host")
209
-            p.key = settings.value("key")
210
-            p.tempTool = settings.value("tool_preheat")
211
-            p.tempBed = settings.value("bed_preheat")
209
+
210
+            # Generic settings
211
+            p.host = settings.value("host", "octopi.local")
212
+            p.apiType = settings.value("api_type", "OctoPrint")
213
+            p.tempTool = settings.value("tool_preheat", "0")
214
+            p.tempBed = settings.value("bed_preheat", "0")
215
+            p.jogSpeed = settings.value("jog_speed", self.jogMoveSpeedDefault)
216
+            p.jogLength = settings.value("jog_length", self.jogMoveLengthDefault)
217
+
218
+            # Octoprint specific settings
219
+            p.key = settings.value("key", "")
220
+
221
+            # Moonraker specific settings
222
+            p.webcam = settings.value("webcam", "0")
223
+
224
+            print("readSettings() " + str(i) + ":\n" + str(p) + "\n")
212
             printers.append(p)
225
             printers.append(p)
213
         settings.endArray()
226
         settings.endArray()
214
         return printers
227
         return printers
216
     def writeSettings(self, printers):
229
     def writeSettings(self, printers):
217
         settings = QSettings(self.vendor, self.name)
230
         settings = QSettings(self.vendor, self.name)
218
 
231
 
219
-        settings.setValue("jog_speed", self.jogMoveSpeed)
220
-        settings.setValue("jog_length", self.jogMoveLength)
221
-
222
         settings.remove("printers")
232
         settings.remove("printers")
223
         settings.beginWriteArray("printers")
233
         settings.beginWriteArray("printers")
224
         for i in range(0, len(printers)):
234
         for i in range(0, len(printers)):
225
             p = printers[i]
235
             p = printers[i]
236
+            print("writeSettings() " + str(i) + ":\n" + str(p) + "\n")
237
+
226
             settings.setArrayIndex(i)
238
             settings.setArrayIndex(i)
239
+
240
+            # Generic settings
227
             settings.setValue("host", p.host)
241
             settings.setValue("host", p.host)
228
-            settings.setValue("key", p.key)
242
+            settings.setValue("api_type", p.apiType)
229
             settings.setValue("tool_preheat", p.tempTool)
243
             settings.setValue("tool_preheat", p.tempTool)
230
             settings.setValue("bed_preheat", p.tempBed)
244
             settings.setValue("bed_preheat", p.tempBed)
245
+            settings.setValue("jog_speed", p.jogSpeed)
246
+            settings.setValue("jog_length", p.jogLength)
247
+
248
+            # Octoprint specific settings
249
+            settings.setValue("key", p.key)
250
+
251
+            # Moonraker specific settings
252
+            settings.setValue("webcam", p.webcam)
231
         settings.endArray()
253
         settings.endArray()
232
         del settings
254
         del settings
233
 
255
 

+ 261
- 133
src/SettingsWindow.py View File

7
 # UI for changes to application configuration.
7
 # UI for changes to application configuration.
8
 
8
 
9
 import string
9
 import string
10
+import pprint
10
 from PyQt5 import QtWidgets
11
 from PyQt5 import QtWidgets
11
-from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLineEdit, QGridLayout
12
+from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLineEdit, QGridLayout, QComboBox
12
 from PyQt5.QtGui import QFontDatabase, QIntValidator
13
 from PyQt5.QtGui import QFontDatabase, QIntValidator
13
 from PyQt5.QtCore import Qt
14
 from PyQt5.QtCore import Qt
14
 
15
 
15
 class Printer(object):
16
 class Printer(object):
16
-    pass
17
+    # field 'api' for actual I/O
18
+    # field 'host' etc. for settings
19
+
20
+    def __repr__(self):
21
+        return pprint.pformat(vars(self))
17
 
22
 
18
 class SettingsWindow(QWidget):
23
 class SettingsWindow(QWidget):
19
-    columns = [ "Hostname", "API Key", "Tool Preheat", "Bed Preheat" ]
20
-    presets = [ "octopi.local", "000000000_API_KEY_HERE_000000000", "0", "0" ]
24
+    genericColumns = [
25
+        ( "Hostname", "octopi.local" ),
26
+        ( "Interface", "OctoPrint" ),
27
+        ( "Tool Temp", "0" ),
28
+        ( "Bed Temp", "0" ),
29
+        ( "Jog Speed", "600" ),
30
+        ( "Jog Length", "10" ),
31
+    ]
32
+
33
+    apiColumns = [
34
+        ( "OctoPrint", [
35
+            ( "API Key", "000000000_API_KEY_HERE_000000000", [] )
36
+        ]),
37
+        ( "Moonraker", [
38
+            ( "Webcam", "0", [] )
39
+        ]),
40
+    ]
21
 
41
 
22
     def __init__(self, parent, *args, **kwargs):
42
     def __init__(self, parent, *args, **kwargs):
23
         super(SettingsWindow, self).__init__(*args, **kwargs)
43
         super(SettingsWindow, self).__init__(*args, **kwargs)
29
         box = QVBoxLayout()
49
         box = QVBoxLayout()
30
         self.setLayout(box)
50
         self.setLayout(box)
31
 
51
 
32
-        staticSettings = QGridLayout()
33
-        box.addLayout(staticSettings, 0)
34
-
35
-        self.jogSpeedText = QLabel("Jog Speed")
36
-        staticSettings.addWidget(self.jogSpeedText, 0, 0)
37
-
38
-        self.jogSpeed = QLineEdit(str(self.parent.jogMoveSpeed))
39
-        self.jogSpeed.setValidator(QIntValidator(1, 6000))
40
-        staticSettings.addWidget(self.jogSpeed, 0, 1)
41
-
42
-        self.jogSpeedUnitText = QLabel("mm/min")
43
-        staticSettings.addWidget(self.jogSpeedUnitText, 0, 2)
44
-
45
-        self.jogLengthText = QLabel("Jog Length")
46
-        staticSettings.addWidget(self.jogLengthText, 1, 0)
47
-
48
-        self.jogLength = QLineEdit(str(self.parent.jogMoveLength))
49
-        self.jogLength.setValidator(QIntValidator(1, 100))
50
-        staticSettings.addWidget(self.jogLength, 1, 1)
51
-
52
-        self.jogLengthUnitText = QLabel("mm")
53
-        staticSettings.addWidget(self.jogLengthUnitText, 1, 2)
54
-
55
-        helpText = "Usage:\n"
56
-        helpText += "1st Column: Printer Hostname or IP address\n"
57
-        helpText += "2nd Column: OctoPrint API Key (32 char hexadecimal)\n"
58
-        helpText += "3rd Column: Tool Preheat Temperature (0 to disable)\n"
59
-        helpText += "4th Column: Bed Preheat Temperature (0 to disable)"
60
-        self.helpText = QLabel(helpText)
61
-        box.addWidget(self.helpText, 0)
62
-        box.setAlignment(self.helpText, Qt.AlignHCenter)
52
+        self.openWeb = QPushButton("&Open Web UI of selected")
53
+        self.openWeb.clicked.connect(self.openWebUI)
54
+        box.addWidget(self.openWeb, 0)
63
 
55
 
64
         buttons = QHBoxLayout()
56
         buttons = QHBoxLayout()
65
         box.addLayout(buttons, 0)
57
         box.addLayout(buttons, 0)
72
         self.remove.clicked.connect(self.removePrinter)
64
         self.remove.clicked.connect(self.removePrinter)
73
         buttons.addWidget(self.remove)
65
         buttons.addWidget(self.remove)
74
 
66
 
75
-        printers = self.parent.readSettings()
76
-        self.rows = len(printers)
77
-        self.table = QTableWidget(self.rows, len(self.columns))
78
-        box.addWidget(self.table, 1)
67
+        buttons2 = QHBoxLayout()
68
+        box.addLayout(buttons2, 0)
79
 
69
 
80
-        for i in range(0, self.rows):
81
-            p = printers[i]
70
+        self.up = QPushButton("Move &Up")
71
+        self.up.clicked.connect(self.moveUp)
72
+        buttons2.addWidget(self.up)
82
 
73
 
74
+        self.down = QPushButton("Move &Down")
75
+        self.down.clicked.connect(self.moveDown)
76
+        buttons2.addWidget(self.down)
77
+
78
+        # Printer data from OctoTray settings
79
+        self.data = self.parent.readSettings()
80
+        self.originalData = self.data.copy()
81
+
82
+        # Table of printers
83
+        self.printerCount = len(self.data)
84
+        self.printers = QTableWidget(self.printerCount, len(self.genericColumns))
85
+        box.addWidget(self.printers, 1)
86
+
87
+        # Populate table of printers
88
+        for i in range(0, self.printerCount):
89
+            p = self.data[i]
90
+
91
+            # hostname in first column
83
             item = QTableWidgetItem(p.host)
92
             item = QTableWidgetItem(p.host)
84
-            self.table.setItem(i, 0, item)
93
+            self.printers.setItem(i, 0, item)
85
 
94
 
86
-            item = QTableWidgetItem(p.key)
87
-            self.table.setItem(i, 1, item)
88
             font = item.font()
95
             font = item.font()
89
             font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
96
             font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
90
             item.setFont(font)
97
             item.setFont(font)
91
 
98
 
92
-            if p.tempTool == None:
93
-                item = QTableWidgetItem("0")
94
-            else:
95
-                item = QTableWidgetItem(p.tempTool)
96
-            self.table.setItem(i, 2, item)
99
+            # API selection in second column
100
+            item = QComboBox()
101
+            for api, options in self.apiColumns:
102
+                item.addItem(api)
103
+                if p.apiType == api:
104
+                    item.setCurrentText(api)
105
+            item.currentIndexChanged.connect(self.selectionChanged)
106
+            self.printers.setCellWidget(i, 1, item)
97
 
107
 
98
-            if p.tempBed == None:
99
-                item = QTableWidgetItem("0")
100
-            else:
101
-                item = QTableWidgetItem(p.tempBed)
102
-            self.table.setItem(i, 3, item)
108
+            # Tool Temp in third column
109
+            item = QTableWidgetItem(p.tempTool)
110
+            self.printers.setItem(i, 2, item)
103
 
111
 
104
-        buttons2 = QHBoxLayout()
105
-        box.addLayout(buttons2, 0)
112
+            # Bed Temp in fourth column
113
+            item = QTableWidgetItem(p.tempBed)
114
+            self.printers.setItem(i, 3, item)
106
 
115
 
107
-        self.up = QPushButton("Move &Up")
108
-        self.up.clicked.connect(self.moveUp)
109
-        buttons2.addWidget(self.up)
116
+            # Jog Speed in fifth column
117
+            item = QTableWidgetItem(p.jogSpeed)
118
+            self.printers.setItem(i, 4, item)
110
 
119
 
111
-        self.down = QPushButton("Move &Down")
112
-        self.down.clicked.connect(self.moveDown)
113
-        buttons2.addWidget(self.down)
120
+            # Jog Length in sixth column
121
+            item = QTableWidgetItem(p.jogLength)
122
+            self.printers.setItem(i, 5, item)
114
 
123
 
115
-        self.openWeb = QPushButton("&Open Web UI of selected")
116
-        self.openWeb.clicked.connect(self.openWebUI)
117
-        box.addWidget(self.openWeb, 0)
124
+            self.apiColumns[0][1][0][2].append(p.key)
125
+            self.apiColumns[1][1][0][2].append(p.webcam)
126
+
127
+        # Table of settings
128
+        self.settings = QTableWidget(1, 2)
129
+        box.addWidget(self.settings, 1)
130
+
131
+        # Callback to update settings when printers selection changes
132
+        self.printers.itemSelectionChanged.connect(self.selectionChanged)
133
+        self.settings.itemChanged.connect(self.settingsChanged)
134
+
135
+        # Put usage hint in settings table
136
+        self.populateDefaultSettings()
118
 
137
 
119
-        self.table.setHorizontalHeaderLabels(self.columns)
120
-        self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
121
-        self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows);
122
-        self.table.resizeColumnsToContents()
138
+        # Setup tables
139
+        self.setupTableHeaders()
123
 
140
 
124
-        if self.rows <= 0:
141
+        # Initialize empty entry when none are available
142
+        if len(self.data) <= 0:
125
             self.addPrinter()
143
             self.addPrinter()
126
 
144
 
127
-    def tableToList(self):
128
-        printers = []
129
-        for i in range(0, self.rows):
130
-            p = Printer()
145
+    def setupTableHeaders(self):
146
+        for t, tc in [
147
+            ( self.printers, [ i[0] for i in self.genericColumns ] ),
148
+            ( self.settings, [ "Option", "Value" ] )
149
+        ]:
150
+            t.setHorizontalHeaderLabels(tc)
151
+            t.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
152
+            t.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows);
153
+            t.resizeColumnsToContents()
154
+
155
+    def settingsChanged(self, item):
156
+        printer = self.printers.currentRow()
157
+        if (printer < 0) or (item.column() < 1):
158
+            return
159
+
160
+        apiType = self.printers.cellWidget(printer, 1).currentText()
161
+        if apiType == self.apiColumns[0][0]:
162
+            self.apiColumns[0][1][item.row()][2][printer] = item.text()
163
+        elif apiType == self.apiColumns[1][0]:
164
+            self.apiColumns[1][1][item.row()][2][printer] = item.text()
165
+
166
+    def populateDefaultSettings(self):
167
+        self.settings.clear()
168
+        self.settings.setRowCount(1)
169
+        self.settings.setColumnCount(2)
170
+
171
+        item = QTableWidgetItem("Select printer for")
172
+        self.settings.setItem(0, 0, item)
173
+        item = QTableWidgetItem("detailed settings")
174
+        self.settings.setItem(0, 1, item)
175
+
176
+        self.setupTableHeaders()
177
+        self.settings.resizeColumnsToContents()
178
+
179
+    def selectionChanged(self):
180
+        i = self.printers.currentRow()
181
+        apiType = self.printers.cellWidget(i, 1).currentText()
182
+        for api, nv in self.apiColumns:
183
+            if api == apiType:
184
+                self.settings.clear()
185
+                self.settings.setRowCount(len(nv))
186
+                self.settings.setColumnCount(2)
187
+
188
+                n = 0
189
+                for name, value, data in nv:
190
+                    item = QTableWidgetItem(name)
191
+                    self.settings.setItem(n, 0, item)
192
+                    item = QTableWidgetItem(data[i])
193
+                    self.settings.setItem(n, 1, item)
194
+                    n += 1
195
+
196
+                self.setupTableHeaders()
197
+                self.settings.resizeColumnsToContents()
198
+                return
131
 
199
 
132
-            p.host = self.table.item(i, 0).text()
133
-            p.key = self.table.item(i, 1).text()
134
-            p.tempTool = self.table.item(i, 2).text()
135
-            p.tempBed = self.table.item(i, 3).text()
200
+        self.populateDefaultSettings()
136
 
201
 
137
-            if p.tempTool == "0":
138
-                p.tempTool = None
202
+    def printersToList(self):
203
+        printers = []
204
+        for i in range(0, self.printerCount):
205
+            p = Printer()
139
 
206
 
140
-            if p.tempBed == "0":
141
-                p.tempBed = None
207
+            p.host = self.printers.item(i, 0).text()
208
+            p.apiType = self.printers.cellWidget(i, 1).currentText()
209
+            p.tempTool = self.printers.item(i, 2).text()
210
+            p.tempBed = self.printers.item(i, 3).text()
211
+            p.jogSpeed = self.printers.item(i, 4).text()
212
+            p.jogLength = self.printers.item(i, 5).text()
213
+            p.key = self.apiColumns[0][1][0][2][i]
214
+            p.webcam = self.apiColumns[1][1][0][2][i]
142
 
215
 
143
             printers.append(p)
216
             printers.append(p)
144
         return printers
217
         return printers
148
             # p.host needs to be valid hostname or IP
221
             # p.host needs to be valid hostname or IP
149
             # TODO
222
             # TODO
150
 
223
 
151
-            # p.key needs to be valid API key (hexadecimal, 32 chars)
152
-            if (len(p.key) != 32) or not all(c in string.hexdigits for c in p.key):
153
-                return (False, "API Key not 32-digit hexadecimal")
224
+            # p.apiType
225
+            # TODO
226
+
227
+            if p.apiType == self.apiColumns[0][0]:
228
+                # p.key only for octoprint
229
+                # p.key needs to be valid API key (hexadecimal, 32 chars)
230
+                if (len(p.key) != 32) or not all(c in string.hexdigits for c in p.key):
231
+                    return (False, "API Key not 32-digit hexadecimal")
232
+            elif p.apiType == self.apiColumns[1][0]:
233
+                # p.webcam only for moonraker
234
+                if (len(p.webcam) < 1) or (len(p.webcam) > 1) or not all(c in string.digits for c in p.webcam):
235
+                    return (False, "Webcam ID not a number from 0...9")
154
 
236
 
155
             # p.tempTool and p.tempBed need to be integer temperatures (0...999)
237
             # p.tempTool and p.tempBed need to be integer temperatures (0...999)
156
             for s in [ p.tempTool, p.tempBed ]:
238
             for s in [ p.tempTool, p.tempBed ]:
159
                 if (len(s) < 1) or (len(s) > 3) or not all(c in string.digits for c in s):
241
                 if (len(s) < 1) or (len(s) > 3) or not all(c in string.digits for c in s):
160
                     return (False, "Temperature not a number from 0...999")
242
                     return (False, "Temperature not a number from 0...999")
161
 
243
 
162
-        js = int(self.jogSpeed.text())
163
-        if (js < 1) or (js > 6000):
164
-            return (False, "Jog Speed not a number from 1...6000")
244
+                js = p.jogSpeed
245
+                if js == None:
246
+                    js = "0"
247
+                if (len(js) < 1) or (len(js) > 3) or not all(c in string.digits for c in js) or (int(js) < 0) or (int(js) > 6000):
248
+                    return (False, "Jog Speed not a number from 0...6000")
165
 
249
 
166
-        jl = int(self.jogLength.text())
167
-        if (jl < 1) or (jl > 100):
168
-            return (False, "Jog Length not a number from 1...100")
250
+                jl = p.jogLength
251
+                if jl == None:
252
+                    jl = "0"
253
+                if (len(jl) < 1) or (len(jl) > 3) or not all(c in string.digits for c in jl) or (int(jl) < 0) or (int(jl) > 100):
254
+                    return (False, "Jog Length not a number from 0...100")
169
 
255
 
170
         return (True, "")
256
         return (True, "")
171
 
257
 
172
     def printerDiffers(self, a, b):
258
     def printerDiffers(self, a, b):
173
-        if (a.host != b.host) or (a.key != b.key) or (a.tempTool != b.tempTool) or (a.tempBed != b.tempBed):
259
+        if (a.host != b.host) or (a.key != b.key) or (a.tempTool != b.tempTool) or (a.tempBed != b.tempBed) or (a.jogSpeed != b.jogSpeed) or (a.jogLength != b.jogLength) or (a.webcam != b.webcam):
174
             return True
260
             return True
175
         return False
261
         return False
176
 
262
 
186
 
272
 
187
     def closeEvent(self, event):
273
     def closeEvent(self, event):
188
         oldPrinters = self.parent.printers
274
         oldPrinters = self.parent.printers
189
-        newPrinters = self.tableToList()
275
+        newPrinters = self.printersToList()
190
 
276
 
191
         valid, errorText = self.settingsValid(newPrinters)
277
         valid, errorText = self.settingsValid(newPrinters)
192
         if valid == False:
278
         if valid == False:
198
                 self.parent.removeSettingsWindow()
284
                 self.parent.removeSettingsWindow()
199
                 return
285
                 return
200
 
286
 
201
-        js = int(self.jogSpeed.text())
202
-        jl = int(self.jogLength.text())
203
-
204
-        if self.printersDiffer(oldPrinters, newPrinters) or (js != self.parent.jogMoveSpeed) or (jl != self.parent.jogMoveLength):
287
+        if self.printersDiffer(oldPrinters, newPrinters):
205
             r = self.parent.showDialog(self.parent.name + " Settings Changed", "Do you want to save the new configuration?", "This will restart the application!", True, False, False)
288
             r = self.parent.showDialog(self.parent.name + " Settings Changed", "Do you want to save the new configuration?", "This will restart the application!", True, False, False)
206
             if r == True:
289
             if r == True:
207
-                self.parent.jogMoveSpeed = js
208
-                self.parent.jogMoveLength = jl
209
                 self.parent.writeSettings(newPrinters)
290
                 self.parent.writeSettings(newPrinters)
291
+                self.parent.removeSettingsWindow()
210
                 self.parent.restartApp()
292
                 self.parent.restartApp()
211
 
293
 
212
         self.parent.removeSettingsWindow()
294
         self.parent.removeSettingsWindow()
213
 
295
 
214
     def addPrinter(self):
296
     def addPrinter(self):
215
-        self.rows += 1
216
-        self.table.setRowCount(self.rows)
217
-        for i in range(0, len(self.columns)):
218
-            item = QTableWidgetItem(self.presets[i])
219
-            self.table.setItem(self.rows - 1, i, item)
220
-            if i == 1:
221
-                font = item.font()
222
-                font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
223
-                item.setFont(font)
224
-        self.table.resizeColumnsToContents()
225
-        self.table.setCurrentItem(self.table.item(self.rows - 1, 0))
297
+        self.printerCount += 1
298
+        self.printers.setRowCount(self.printerCount)
299
+        for i in range(0, len(self.genericColumns)):
300
+            if i != 1:
301
+                item = QTableWidgetItem(self.genericColumns[i][1])
302
+                self.printers.setItem(self.printerCount - 1, i, item)
303
+                if i == 0:
304
+                    font = item.font()
305
+                    font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
306
+                    item.setFont(font)
307
+            else:
308
+                item = QComboBox()
309
+                for api, options in self.apiColumns:
310
+                    item.addItem(api)
311
+                    if self.genericColumns[i][1] == api:
312
+                        item.setCurrentText(api)
313
+                item.currentIndexChanged.connect(self.selectionChanged)
314
+                self.printers.setCellWidget(self.printerCount - 1, i, item)
315
+
316
+        # add default values for api specific settings
317
+        self.apiColumns[0][1][0][2].append(self.apiColumns[0][1][0][1])
318
+        self.apiColumns[1][1][0][2].append(self.apiColumns[1][1][0][1])
319
+
320
+        self.printers.resizeColumnsToContents()
321
+        self.printers.setCurrentItem(self.printers.item(self.printerCount - 1, 0))
226
 
322
 
227
     def removePrinter(self):
323
     def removePrinter(self):
228
-        r = self.table.currentRow()
229
-        if (r >= 0) and (r < self.rows):
230
-            self.rows -= 1
231
-            self.table.removeRow(r)
232
-            self.table.setCurrentItem(self.table.item(min(r, self.rows - 1), 0))
324
+        r = self.printers.currentRow()
325
+        if (r >= 0) and (r < self.printerCount):
326
+            self.printerCount -= 1
327
+            self.printers.removeRow(r)
328
+            self.printers.setCurrentItem(self.printers.item(min(r, self.printerCount - 1), 0))
329
+
330
+            # also remove values for api specific settings
331
+            del self.apiColumns[0][1][0][2][r]
332
+            del self.apiColumns[1][1][0][2][r]
233
 
333
 
234
     def moveUp(self):
334
     def moveUp(self):
235
-        i = self.table.currentRow()
335
+        i = self.printers.currentRow()
236
         if i <= 0:
336
         if i <= 0:
237
             return
337
             return
238
-        host = self.table.item(i, 0).text()
239
-        key = self.table.item(i, 1).text()
240
-        self.table.item(i, 0).setText(self.table.item(i - 1, 0).text())
241
-        self.table.item(i, 1).setText(self.table.item(i - 1, 1).text())
242
-        self.table.item(i - 1, 0).setText(host)
243
-        self.table.item(i - 1, 1).setText(key)
244
-        self.table.setCurrentItem(self.table.item(i - 1, 0))
338
+
339
+        for c in range(0, self.printers.columnCount()):
340
+            if c != 1:
341
+                a = self.printers.takeItem(i, c)
342
+                b = self.printers.takeItem(i - 1, c)
343
+                self.printers.setItem(i, c, b)
344
+                self.printers.setItem(i - 1, c, a)
345
+            else:
346
+                a = self.printers.cellWidget(i, c).currentText()
347
+                b = self.printers.cellWidget(i - 1, c).currentText()
348
+                self.printers.cellWidget(i, c).setCurrentText(b)
349
+                self.printers.cellWidget(i - 1, c).setCurrentText(a)
350
+
351
+        # also move values for api specific settings
352
+        for v in [ self.apiColumns[0][1][0][2], self.apiColumns[1][1][0][2] ]:
353
+            a = v[i]
354
+            b = v[i - 1]
355
+            v[i] = b
356
+            v[i - 1] = a
357
+
358
+        self.printers.setCurrentItem(self.printers.item(i - 1, 0))
245
 
359
 
246
     def moveDown(self):
360
     def moveDown(self):
247
-        i = self.table.currentRow()
248
-        if i >= (self.rows - 1):
361
+        i = self.printers.currentRow()
362
+        if i >= (self.printerCount - 1):
249
             return
363
             return
250
-        host = self.table.item(i, 0).text()
251
-        key = self.table.item(i, 1).text()
252
-        self.table.item(i, 0).setText(self.table.item(i + 1, 0).text())
253
-        self.table.item(i, 1).setText(self.table.item(i + 1, 1).text())
254
-        self.table.item(i + 1, 0).setText(host)
255
-        self.table.item(i + 1, 1).setText(key)
256
-        self.table.setCurrentItem(self.table.item(i + 1, 0))
364
+
365
+        for c in range(0, self.printers.columnCount()):
366
+            if c != 1:
367
+                a = self.printers.takeItem(i, c)
368
+                b = self.printers.takeItem(i + 1, c)
369
+                self.printers.setItem(i, c, b)
370
+                self.printers.setItem(i + 1, c, a)
371
+            else:
372
+                a = self.printers.cellWidget(i, c).currentText()
373
+                b = self.printers.cellWidget(i + 1, c).currentText()
374
+                self.printers.cellWidget(i, c).setCurrentText(b)
375
+                self.printers.cellWidget(i + 1, c).setCurrentText(a)
376
+
377
+        # also move values for api specific settings
378
+        for v in [ self.apiColumns[0][1][0][2], self.apiColumns[1][1][0][2] ]:
379
+            a = v[i]
380
+            b = v[i + 1]
381
+            v[i] = b
382
+            v[i + 1] = a
383
+
384
+        self.printers.setCurrentItem(self.printers.item(i + 1, 0))
257
 
385
 
258
     def openWebUI(self):
386
     def openWebUI(self):
259
-        host = self.table.item(self.table.currentRow(), 0).text()
387
+        host = self.printers.item(self.printers.currentRow(), 0).text()
260
         self.parent.openBrowser(host)
388
         self.parent.openBrowser(host)

Loading…
Cancel
Save