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,7 +1,7 @@
1 1
 # OctoTray Linux Qt client
2 2
 
3 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 5
 For the implementation it is using PyQt5.
6 6
 Automatic builds are provided for Linux, Windows and macOS.
7 7
 

+ 468
- 0
src/APIMoonraker.py View File

@@ -0,0 +1,468 @@
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,10 +25,10 @@ class APIOctoprint():
25 25
 
26 26
     # return list of tuples ( "name", func(name) )
27 27
     # with all available commands.
28
-    # call function in with name of action!
28
+    # call function with name of action!
29 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 33
         commands = []
34 34
 
@@ -52,6 +52,7 @@ class APIOctoprint():
52 52
     # HTTP API #
53 53
     ############
54 54
 
55
+    # only used internally
55 56
     def sendRequest(self, headers, path, content = None):
56 57
         url = "http://" + self.host + "/api/" + path
57 58
         if content == None:
@@ -71,6 +72,7 @@ class APIOctoprint():
71 72
             print("Timeout waiting for response to \"" + url + "\"")
72 73
             return "timeout"
73 74
 
75
+    # only used internally
74 76
     def sendPostRequest(self, path, content):
75 77
         headers = {
76 78
             "Content-Type": "application/json",
@@ -78,6 +80,7 @@ class APIOctoprint():
78 80
         }
79 81
         return self.sendRequest(headers, path, content)
80 82
 
83
+    # only used internally
81 84
     def sendGetRequest(self, path):
82 85
         headers = {
83 86
             "X-Api-Key": self.key
@@ -88,20 +91,19 @@ class APIOctoprint():
88 91
     # Command discovery #
89 92
     #####################
90 93
 
91
-    def getMethod(self):
94
+    # only used internally
95
+    def getMethodInternal(self):
92 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 105
         r = self.sendGetRequest("system/commands/custom")
104
-        if r == "timeout":
106
+        if (r == "timeout") or (r == "error"):
105 107
             return "unknown"
106 108
 
107 109
         try:
@@ -117,6 +119,11 @@ class APIOctoprint():
117 119
 
118 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 127
     def getSystemCommands(self):
121 128
         l = []
122 129
         r = self.sendGetRequest("system/commands/custom")
@@ -138,6 +145,7 @@ class APIOctoprint():
138 145
     # Safety Checks #
139 146
     #################
140 147
 
148
+    # only used internally
141 149
     def stateSafetyCheck(self, actionString):
142 150
         state = self.getState()
143 151
         if state.lower() in self.statesWithWarning:
@@ -145,12 +153,14 @@ class APIOctoprint():
145 153
                 return True
146 154
         return False
147 155
 
156
+    # only used internally
148 157
     def tempSafetyCheck(self, actionString):
149 158
         if self.getTemperatureIsSafe() == False:
150 159
             if self.parent.showDialog("OctoTray Warning", "The printer seems to still be hot!", "Do you really want to " + actionString + "?", True, True) == False:
151 160
                 return True
152 161
         return False
153 162
 
163
+    # only used internally
154 164
     def safetyCheck(self, actionString):
155 165
         if self.stateSafetyCheck(actionString):
156 166
             return True
@@ -162,6 +172,7 @@ class APIOctoprint():
162 172
     # Power Toggling #
163 173
     ##################
164 174
 
175
+    # only used internally (passed to caller as a pointer)
165 176
     def callSystemCommand(self, name):
166 177
         if "off" in name.lower():
167 178
             if self.safetyCheck("run '" + name + "'"):
@@ -170,6 +181,7 @@ class APIOctoprint():
170 181
         cmd = urllib.parse.quote(name)
171 182
         self.sendPostRequest("system/commands/custom/" + cmd, '')
172 183
 
184
+    # only used internally (passed to caller as a pointer)
173 185
     def setPower(self, name):
174 186
         if "off" in name.lower():
175 187
             if self.safetyCheck(name):
@@ -181,6 +193,7 @@ class APIOctoprint():
181 193
 
182 194
         return self.sendPostRequest("plugin/psucontrol", '{ "command":"' + cmd + '" }')
183 195
 
196
+    # should automatically turn on printer, regardless of method
184 197
     def turnOn(self):
185 198
         if self.method == "psucontrol":
186 199
             self.setPower("on")
@@ -191,6 +204,7 @@ class APIOctoprint():
191 204
                     self.callSystemCommand(cmd)
192 205
                     break
193 206
 
207
+    # should automatically turn off printer, regardless of method
194 208
     def turnOff(self):
195 209
         if self.method == "psucontrol":
196 210
             self.setPower("off")
@@ -205,6 +219,7 @@ class APIOctoprint():
205 219
     # Status Information #
206 220
     ######################
207 221
 
222
+    # only used internally
208 223
     def getTemperatureIsSafe(self, limit = 50.0):
209 224
         r = self.sendGetRequest("printer")
210 225
         try:
@@ -222,6 +237,7 @@ class APIOctoprint():
222 237
             pass
223 238
         return True
224 239
 
240
+    # human readable temperatures
225 241
     def getTemperatureString(self):
226 242
         r = self.sendGetRequest("printer")
227 243
         s = ""
@@ -261,6 +277,7 @@ class APIOctoprint():
261 277
                 s += " "
262 278
         return s.strip()
263 279
 
280
+    # only used internally
264 281
     def getState(self):
265 282
         r = self.sendGetRequest("job")
266 283
         try:
@@ -271,6 +288,7 @@ class APIOctoprint():
271 288
             pass
272 289
         return "Unknown"
273 290
 
291
+    # only used internally
274 292
     def getProgress(self):
275 293
         r = self.sendGetRequest("job")
276 294
         try:
@@ -281,6 +299,7 @@ class APIOctoprint():
281 299
             pass
282 300
         return "Unknown"
283 301
 
302
+    # human readable name (fall back to hostname)
284 303
     def getName(self):
285 304
         r = self.sendGetRequest("printerprofiles")
286 305
         try:
@@ -293,6 +312,7 @@ class APIOctoprint():
293 312
             pass
294 313
         return self.host
295 314
 
315
+    # human readable progress
296 316
     def getProgressString(self):
297 317
         s = ""
298 318
         progress = self.getProgress()
@@ -320,7 +340,7 @@ class APIOctoprint():
320 340
 
321 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 344
         if self.stateSafetyCheck("move it"):
325 345
             return
326 346
 
@@ -328,7 +348,7 @@ class APIOctoprint():
328 348
         if relative == False:
329 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 353
     def callPauseResume(self):
334 354
         if self.stateSafetyCheck("pause/resume"):
@@ -342,7 +362,7 @@ class APIOctoprint():
342 362
 
343 363
     def statusDialog(self):
344 364
         progress = self.getProgress()
345
-        s = self.host + "\n"
365
+        s = self.getName() + "\n"
346 366
         warning = False
347 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 368
             s += "%.1f%% Completion\n" % progress["completion"]
@@ -383,16 +403,17 @@ class APIOctoprint():
383 403
     # Temperature #
384 404
     ###############
385 405
 
406
+    # only used internally
386 407
     def setTemperature(self, what, temp):
408
+        if temp == None:
409
+            temp = 0
410
+
387 411
         path = "printer/bed"
388
-        s = "{\"command\": \"target\", \"target\": " + temp + "}"
412
+        s = "{\"command\": \"target\", \"target\": " + str(temp) + "}"
389 413
 
390 414
         if "tool" in what:
391 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 418
         self.sendPostRequest(path, s)
398 419
 
@@ -408,3 +429,10 @@ class APIOctoprint():
408 429
 
409 430
         self.setTemperature("tool0", 0)
410 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,7 +28,9 @@ class CamWindow(QWidget):
28 28
         self.manager.finished.connect(self.handleResponse)
29 29
         self.parent = parent
30 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 35
         self.setWindowTitle(parent.name + " Webcam Stream")
34 36
         self.setWindowIcon(parent.icon)
@@ -163,22 +165,22 @@ class CamWindow(QWidget):
163 165
         self.printer.api.callJobCancel()
164 166
 
165 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 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 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 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 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 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 185
     def homeX(self):
184 186
         self.printer.api.callHoming("x")
@@ -199,7 +201,7 @@ class CamWindow(QWidget):
199 201
         self.printer.api.turnOff()
200 202
 
201 203
     def cooldown(self):
202
-        self.printer.api.printerCooldown(self.printer)
204
+        self.printer.api.printerCooldown()
203 205
 
204 206
     def preheatTool(self):
205 207
         self.printer.api.printerHeatTool(self.printer.tempTool)
@@ -251,17 +253,22 @@ class CamWindow(QWidget):
251 253
         self.scheduleLoadStatus()
252 254
 
253 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,13 +14,10 @@ from PyQt5.QtGui import QIcon, QPixmap, QDesktopServices, QCursor
14 14
 from PyQt5.QtCore import QCoreApplication, QSettings, QUrl
15 15
 from CamWindow import CamWindow
16 16
 from SettingsWindow import SettingsWindow
17
+from SettingsWindow import Printer
17 18
 from MainWindow import MainWindow
18 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 22
 class OctoTray():
26 23
     name = "OctoTray"
@@ -46,8 +43,8 @@ class OctoTray():
46 43
     settingsWindow = None
47 44
 
48 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 49
     def __init__(self, app, inSysTray):
53 50
         QCoreApplication.setApplicationName(self.name)
@@ -60,9 +57,21 @@ class OctoTray():
60 57
 
61 58
         unknownCount = 0
62 59
         for p in self.printers:
63
-            p.api = APIOctoprint(self, p.host, p.key)
64 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 75
             commands = p.api.getAvailableCommands()
67 76
 
68 77
             # don't populate menu when no methods are available
@@ -83,7 +92,7 @@ class OctoTray():
83 92
             for cmd in commands:
84 93
                 name, func = cmd
85 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 96
                 p.menus.append(action)
88 97
                 menu.addAction(action)
89 98
 
@@ -192,23 +201,27 @@ class OctoTray():
192 201
     def readSettings(self):
193 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 204
         printers = []
204 205
         l = settings.beginReadArray("printers")
205 206
         for i in range(0, l):
206 207
             settings.setArrayIndex(i)
207 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 225
             printers.append(p)
213 226
         settings.endArray()
214 227
         return printers
@@ -216,18 +229,27 @@ class OctoTray():
216 229
     def writeSettings(self, printers):
217 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 232
         settings.remove("printers")
223 233
         settings.beginWriteArray("printers")
224 234
         for i in range(0, len(printers)):
225 235
             p = printers[i]
236
+            print("writeSettings() " + str(i) + ":\n" + str(p) + "\n")
237
+
226 238
             settings.setArrayIndex(i)
239
+
240
+            # Generic settings
227 241
             settings.setValue("host", p.host)
228
-            settings.setValue("key", p.key)
242
+            settings.setValue("api_type", p.apiType)
229 243
             settings.setValue("tool_preheat", p.tempTool)
230 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 253
         settings.endArray()
232 254
         del settings
233 255
 

+ 261
- 133
src/SettingsWindow.py View File

@@ -7,17 +7,37 @@
7 7
 # UI for changes to application configuration.
8 8
 
9 9
 import string
10
+import pprint
10 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 13
 from PyQt5.QtGui import QFontDatabase, QIntValidator
13 14
 from PyQt5.QtCore import Qt
14 15
 
15 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 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 42
     def __init__(self, parent, *args, **kwargs):
23 43
         super(SettingsWindow, self).__init__(*args, **kwargs)
@@ -29,37 +49,9 @@ class SettingsWindow(QWidget):
29 49
         box = QVBoxLayout()
30 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 56
         buttons = QHBoxLayout()
65 57
         box.addLayout(buttons, 0)
@@ -72,73 +64,154 @@ class SettingsWindow(QWidget):
72 64
         self.remove.clicked.connect(self.removePrinter)
73 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 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 95
             font = item.font()
89 96
             font.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
90 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 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 216
             printers.append(p)
144 217
         return printers
@@ -148,9 +221,18 @@ class SettingsWindow(QWidget):
148 221
             # p.host needs to be valid hostname or IP
149 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 237
             # p.tempTool and p.tempBed need to be integer temperatures (0...999)
156 238
             for s in [ p.tempTool, p.tempBed ]:
@@ -159,18 +241,22 @@ class SettingsWindow(QWidget):
159 241
                 if (len(s) < 1) or (len(s) > 3) or not all(c in string.digits for c in s):
160 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 256
         return (True, "")
171 257
 
172 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 260
             return True
175 261
         return False
176 262
 
@@ -186,7 +272,7 @@ class SettingsWindow(QWidget):
186 272
 
187 273
     def closeEvent(self, event):
188 274
         oldPrinters = self.parent.printers
189
-        newPrinters = self.tableToList()
275
+        newPrinters = self.printersToList()
190 276
 
191 277
         valid, errorText = self.settingsValid(newPrinters)
192 278
         if valid == False:
@@ -198,63 +284,105 @@ class SettingsWindow(QWidget):
198 284
                 self.parent.removeSettingsWindow()
199 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 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 289
             if r == True:
207
-                self.parent.jogMoveSpeed = js
208
-                self.parent.jogMoveLength = jl
209 290
                 self.parent.writeSettings(newPrinters)
291
+                self.parent.removeSettingsWindow()
210 292
                 self.parent.restartApp()
211 293
 
212 294
         self.parent.removeSettingsWindow()
213 295
 
214 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 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 334
     def moveUp(self):
235
-        i = self.table.currentRow()
335
+        i = self.printers.currentRow()
236 336
         if i <= 0:
237 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 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 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 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 388
         self.parent.openBrowser(host)

Loading…
Cancel
Save