Added job auto pausing options, fixed job time estimation.

master
Martin 2018-10-22 03:31:21 +02:00
rodzic 1d638e1f1d
commit 73ef2c792a
3 zmienionych plików z 138 dodań i 77 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
from tkinter import Tk, Label, Button, Entry, Menu, filedialog, messagebox, colorchooser, Canvas, Frame, LabelFrame, Scale, Toplevel, PhotoImage from tkinter import Tk, Label, Button, Checkbutton, Entry, Menu, filedialog, messagebox, colorchooser, Canvas, Frame, LabelFrame, Scale, Toplevel, PhotoImage, IntVar
from tkinter.font import Font from tkinter.font import Font
import tkinter.ttk as ttk import tkinter.ttk as ttk
from tkinter import LEFT, TOP, BOTTOM, N, YES, W,SUNKEN,X, HORIZONTAL, DISABLED, NORMAL, RAISED, FLAT, RIDGE, END from tkinter import LEFT, TOP, BOTTOM, N, YES, W,SUNKEN,X, HORIZONTAL, DISABLED, NORMAL, RAISED, FLAT, RIDGE, END
@ -8,6 +8,7 @@ from path_preview import ResizingCanvas, load_gcode_file, save_gcode_file, load_
from collections import namedtuple from collections import namedtuple
import copy, re, math, time, pickle import copy, re, math, time, pickle
#import control_serial_mockup as serial
import control_serial as serial import control_serial as serial
class ControlAppGUI: class ControlAppGUI:
@ -88,6 +89,15 @@ class ControlAppGUI:
self.stopButton = Button(tab1, text="STOP", command=self.StopAll, state=DISABLED) self.stopButton = Button(tab1, text="STOP", command=self.StopAll, state=DISABLED)
self.stopButton.grid(row=4,column=0) self.stopButton.grid(row=4,column=0)
self.pauseOnToolChange = IntVar()
self.pauseOnTrim = IntVar()
self.toolChangeCheck = Checkbutton(tab1, variable=self.pauseOnToolChange, onvalue=1, offvalue=0, text="Pause on tool change")
self.toolChangeCheck.grid(row=4,column=1)
self.toolChangeCheck.select()
self.trimCheck = Checkbutton(tab1, variable=self.pauseOnTrim, onvalue=1, offvalue=0, text="Pause on trim")
self.trimCheck.grid(row=4,column=2)
self.trimCheck.select()
progressFrame = Frame(tab1) progressFrame = Frame(tab1)
Label(progressFrame, text="Tool changes: ", bd=1).grid(row=0,column=0) Label(progressFrame, text="Tool changes: ", bd=1).grid(row=0,column=0)
self.toolChangesLabel = Label(progressFrame, text="0/0", bd=1, relief=SUNKEN) self.toolChangesLabel = Label(progressFrame, text="0/0", bd=1, relief=SUNKEN)
@ -211,11 +221,11 @@ class ControlAppGUI:
self.toolPointsTotal, self.toolChangesTotal, self.distancesList = toolpath_info(self.commands) self.toolPointsTotal, self.toolChangesTotal, self.distancesList = toolpath_info(self.commands)
self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal)) self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal))
self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal)) self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal))
self.timeLabel.config(text="%d/%d" % (self.distancesList[self.currentToolChange]- self.distanceTraveled, self.distancesList[-1]-self.distanceTraveled)) self.UpdateTimeEstLabel()
self.canvas.draw_toolpath(self.commands) self.canvas.draw_toolpath(self.commands)
def About(self): def About(self):
#self.ToolChangePopup() #self.PausePopup()
messagebox.showinfo('About this software', 'This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\nWritten in 2018 by markol.') messagebox.showinfo('About this software', 'This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\nWritten in 2018 by markol.')
def Settings(self): def Settings(self):
tl = Toplevel(root) tl = Toplevel(root)
@ -267,7 +277,7 @@ class ControlAppGUI:
data = {"workAreaSize": self.workAreaSize} data = {"workAreaSize": self.workAreaSize}
pickle.dump(data, f) pickle.dump(data, f)
except Exception as e: except Exception as e:
print ("error while saving settings:", str(e)) print ("Error while saving settings:", str(e))
def ToggleConnect(self): def ToggleConnect(self):
if self.isConnected: if self.isConnected:
@ -294,6 +304,7 @@ class ControlAppGUI:
rectangle = toolpath_border_points(self.commands[1:]) rectangle = toolpath_border_points(self.commands[1:])
for point in rectangle: for point in rectangle:
serial.queue_command("G0 X%f Y%f F5000\n" % point) serial.queue_command("G0 X%f Y%f F5000\n" % point)
def ToggleStart(self): def ToggleStart(self):
if self.isJobPaused: if self.isJobPaused:
serial.queue.clear() serial.queue.clear()
@ -308,51 +319,83 @@ class ControlAppGUI:
self.canvas.clear() self.canvas.clear()
startInstructionIndex = 0 startInstructionIndex = 0
self.start = time.time() self.start = time.time()
# after every move command being sent, this callback is executed
def progressCallback(instruction_index):
point = self.commands[instruction_index]
if self.lastMove:
coord = (self.lastMove[1], self.lastMove[2], point[1], point[2])
color = self.currentColor
# set color for jump move
if "G0" in point[0]:
color = "snow2"
else:
self.currentToolPoint += 1
self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal))
self.distanceTraveled += math.hypot(coord[0] - coord[2], coord[1] - coord[3])
line = self.canvas.create_line(self.canvas.calc_coords(coord), fill=color)
self.canvas.lift(self.canvas.pointer, line)
self.timeLabel.config(text="%d/%d" % (self.distancesList[self.currentToolChange]- self.distanceTraveled, self.distancesList[-1]-self.distanceTraveled))
self.lastSendCommandIndex = instruction_index
self.lastMove = point
# this callback pauses
def progressToolChangeCallback(instruction_index):
point = self.commands[instruction_index]
self.lastSendCommandIndex = instruction_index
self.currentColor = _from_rgb((point[1], point[2], point[3]))
self.ToggleStart()
self.currentToolChange += 1
self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal))
self.ToolChangePopup(self.currentColor)
self.isJobRunning = True self.isJobRunning = True
commandsCount = len(self.commands) self.QueueCommandsBlock(startInstructionIndex)
# all the commands until tool change command, are queued at once
for i in range(startInstructionIndex, commandsCount):
point = self.commands[i]
# pause on color change
if "M6" in point[0]:
serial.queue_command("G0 F25000\n", lambda _, index = i: progressToolChangeCallback(index))
break
else:
serial.queue_command("%s X%f Y%f\n" % (point[0],point[1], point[2]), lambda _, index = i: progressCallback(index))
# queue job finish callback, it is unnecessary added after every pause but is cleaned in pause callback
serial.queue_command("M114\n", self.JobFinished)
self.isJobPaused = not self.isJobPaused self.isJobPaused = not self.isJobPaused
def QueueCommandsBlock(self, startInstructionIndex):
commandsCount = len(self.commands)
def progressCallback(instruction_index):
''' after every move G0 or G1 or G28 command being sent, this callback is executed '''
point = self.commands[instruction_index]
if self.lastMove:
coord = (self.lastMove[1], self.lastMove[2], point[1], point[2])
color = self.currentColor
# set color for jump move
if "G0" == point[0] or "G28" == point[0]:
color = "snow2"
else:
self.currentToolPoint += 1
self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal))
# calculate distance for material usage
self.distanceTraveled += math.hypot(coord[0] - coord[2], coord[1] - coord[3])
# draw new line on canvas
line = self.canvas.create_line(self.canvas.calc_coords(coord), fill=color)
self.canvas.lift(self.canvas.pointer, line)
self.UpdateTimeEstLabel()
# store next start point and instruction index
self.lastSendCommandIndex = instruction_index
self.lastMove = point
def progressPauseCallback(instruction_index, trim = False):
''' this callback pauses the job '''
point = self.commands[instruction_index]
self.lastSendCommandIndex = instruction_index
# pause on color change
if not trim:
self.currentColor = _from_rgb((point[1], point[2], point[3]))
self.currentToolChange += 1
self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal))
# pause enabled or not
if self.pauseOnToolChange.get() == 1:
self.ToggleStart()
self.PausePopup(self.currentColor)
else:
self.QueueCommandsBlock(self.lastSendCommandIndex + 1)
# pause on trim
else:
# pause enabled or not
if self.pauseOnTrim.get() == 1:
self.ToggleStart()
self.PausePopup(self.currentColor, trim)
else:
self.QueueCommandsBlock(self.lastSendCommandIndex + 1)
# all the commands until next tool change command, are queued at once
# unsupported commands are ignored
for i in range(startInstructionIndex, commandsCount):
point = self.commands[i]
# pause on color change
if "M6" == point[0]:
serial.queue_command("M6\n", lambda _, index = i: progressPauseCallback(index))
break
# pause on trim
elif "G12" == point[0]:
# if next command is a color change, there is no need to pause now
if i < commandsCount + 1 and self.commands[i+1][0] == "M6" and self.pauseOnToolChange.get() == 1:
serial.queue_command("G12\n")
else:
serial.queue_command("G12\n", lambda _, index = i: progressPauseCallback(index, trim = True))
break
elif "G1" == point[0] or "G0" == point[0] or "G28" == point[0]:
serial.queue_command("%s X%f Y%f\n" % (point[0],point[1], point[2]), lambda _, index = i: progressCallback(index))
# queue job finish callback
if i + 1 >= commandsCount:
serial.queue_command("M114\n", self.JobFinished)
def SetNavButtonsState(self, enabled = False): def SetNavButtonsState(self, enabled = False):
newState = NORMAL if enabled else DISABLED newState = NORMAL if enabled else DISABLED
for b in self.navigationButtons: for b in self.navigationButtons:
@ -416,14 +459,17 @@ class ControlAppGUI:
self.currentColor = 'black' self.currentColor = 'black'
self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal)) self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal))
self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal)) self.toolChangesLabel.config(text="%d/%d" % (self.currentToolChange, self.toolChangesTotal))
self.timeLabel.config(text="%d/%d" % (self.distancesList[self.currentToolChange]- self.distanceTraveled, self.distancesList[-1]-self.distanceTraveled)) self.UpdateTimeEstLabel()
self.startButton.config(text="Start job") self.startButton.config(text="Start job")
self.status.config(text="Job finished") self.status.config(text="Job finished")
timeTaken = time.time() - self.start timeTaken = time.time() - self.start
# non blocking popup messagebox # non blocking popup messagebox
if messagePopup: if messagePopup:
tl = Toplevel(root) tl = Toplevel(root)
# this pop-up is always on top and other windows are deactivated
tl.attributes('-topmost', 'true')
tl.title("Job finished") tl.title("Job finished")
tl.grab_set()
frame = Frame(tl) frame = Frame(tl)
frame.grid() frame.grid()
Label(frame, text='Current job is finished and took %s.' % time.strftime("%H hours, %M minutes, %S seconds", time.gmtime(timeTaken)) ).grid(row=0, column=0, sticky=N) Label(frame, text='Current job is finished and took %s.' % time.strftime("%H hours, %M minutes, %S seconds", time.gmtime(timeTaken)) ).grid(row=0, column=0, sticky=N)
@ -476,6 +522,12 @@ class ControlAppGUI:
if self.isJobRunning: if self.isJobRunning:
return return
self.canvas.draw_toolpath(self.commands[0:int(val)]) self.canvas.draw_toolpath(self.commands[0:int(val)])
def UpdateTimeEstLabel(self):
# avg milimeters per second factor
factor = 11.0
time_to_toolchange = (self.distancesList[self.currentToolChange]- self.distanceTraveled) / factor
time_total = (self.distancesList[-1]-self.distanceTraveled) / factor
self.timeLabel.config(text="%d m %d s/%d m %d s" % (time_to_toolchange / 60, time_to_toolchange % 60, time_total / 60, time_total % 60))
def GetPositionTimerTaks(self): def GetPositionTimerTaks(self):
if self.isConnected: if self.isConnected:
def TimerCallback(response): def TimerCallback(response):
@ -490,28 +542,28 @@ class ControlAppGUI:
def CleanUp(self): def CleanUp(self):
serial.close_serial() serial.close_serial()
def ToolChangePopup(self, newColor = "black"): def PausePopup(self, newColor = "black", trim = False):
tl = Toplevel(root) tl = Toplevel(root)
tl.attributes('-topmost', 'true')
tl.grab_set()
tl.title("Tool change") tl.title("Tool change")
msg = "change the tool for a next color"
if trim:
tl.title("Thread trim")
msg = "cut the thread"
frame = Frame(tl) frame = Frame(tl)
frame.grid() frame.grid()
canvas = Canvas(frame, width=100, height=130) canvas = Canvas(frame, width=64, height=64)
canvas.grid(row=1, column=0) canvas.grid(row=2, column=0)
#imgvar = PhotoImage(file="pyrocket.png") canvas.create_rectangle(0, 0, 65, 65, fill=newColor)
#canvas.create_image(50,70, image=imgvar) msgbody = Label(frame, text="There is the moment to %s. Resume the current job after change." % msg)
#canvas.image = imgvar msgbody.grid(row=1, column=0, sticky=N)
msgbody1 = Label(frame, text="There is time to change tool for a " )
msgbody1.grid(row=1, column=1, sticky=N)
lang = Label(frame, text="next color", font=Font(size=20, weight="bold"), fg=newColor)
lang.grid(row=1, column=2, sticky=N)
msgbody2 = Label(frame, text="Resume the current job after change.")
msgbody2.grid(row=1, column=3, sticky=N)
okbttn = Button(frame, text="OK", command=lambda: tl.destroy(), width=10) okbttn = Button(frame, text="OK", command=lambda: tl.destroy(), width=10)
okbttn.grid(row=2, column=4) okbttn.grid(row=2, column=2)
root = Tk() root = Tk()
my_gui = ControlAppGUI(root) my_gui = ControlAppGUI(root)

Wyświetl plik

@ -60,7 +60,7 @@ def send_serial(msg, responseCallback = None):
global last_pos global last_pos
response = "ok\n" response = "ok\n"
if "M114" in msg: if "M114" in msg:
response = "X%f Y%f\nok\n" % (last_pos[0],last_pos[1]) response = "X:%f,Y:%f\nok\n" % (last_pos[0],last_pos[1])
if "G0" in msg or "G1" in msg: if "G0" in msg or "G1" in msg:
regex_result = gcode_regexX.search(msg) regex_result = gcode_regexX.search(msg)
if regex_result: if regex_result:
@ -113,4 +113,4 @@ class SendingThread(threading.Thread):
queue.task_done() queue.task_done()
def terminate(self): def terminate(self):
self.running = False self.running = False

Wyświetl plik

@ -83,10 +83,14 @@ class ResizingCanvas(Canvas):
current_color = "black" current_color = "black"
color = current_color color = current_color
for point in points[1:]: for point in points[1:]:
if "G0" in point[0]: if "G0" == point[0]:
color = "snow2" color = "snow2"
elif "M6" in point[0]: elif "M6" == point[0]:
current_color = _from_rgb((point[1], point[2], point[3])) current_color = _from_rgb((point[1], point[2], point[3]))
color = current_color
continue
# commands other than G0, G1, G28 or M6 are ignored for drawing a preview
elif "G1" != point[0] and "G28" != point[0]:
continue continue
coord = (last_point[1], last_point[2], point[1], point[2]) coord = (last_point[1], last_point[2], point[1], point[2])
last_point = point last_point = point
@ -99,7 +103,7 @@ class ResizingCanvas(Canvas):
def translate_toolpath(points, translate=(0,0)): def translate_toolpath(points, translate=(0,0)):
#new_points = copy.deepcopy(points) #new_points = copy.deepcopy(points)
for point in points: for point in points:
if not ("G0" in point[0] or "G1" in point[0]): if not ("G0" == point[0] or "G1" == point[0]):
continue continue
point[1] += translate[0] point[1] += translate[0]
point[2] += translate[1] point[2] += translate[1]
@ -107,7 +111,7 @@ def translate_toolpath(points, translate=(0,0)):
def rotate_toolpath(points, origin, theta): def rotate_toolpath(points, origin, theta):
for point in points: for point in points:
if not ("G0" in point[0] or "G1" in point[0]): if not ("G0" == point[0] or "G1" == point[0]):
continue continue
point[1], point[2] = rotate(point[1:3], origin, theta) point[1], point[2] = rotate(point[1:3], origin, theta)
return points return points
@ -127,21 +131,21 @@ def rotate(point, origin, angle):
def reflect_toolpath(points, d): def reflect_toolpath(points, d):
for point in points: for point in points:
if not ("G0" in point[0] or "G1" in point[0]): if not ("G0" == point[0] or "G1" == point[0]):
continue continue
point[1] = 2*d - point[1] point[1] = 2*d - point[1]
return points return points
def scale_toolpath(points, f): def scale_toolpath(points, f):
for point in points: for point in points:
if not ("G0" in point[0] or "G1" in point[0]): if not ("G0" == point[0] or "G1" == point[0]):
continue continue
point[1] += point[1]*f point[1] += point[1]*f
point[2] += point[2]*f point[2] += point[2]*f
return points return points
def toolpath_border_points(points): def toolpath_border_points(points):
points = [elem for elem in points if "G0" in elem[0] or "G1" in elem[0]] points = [elem for elem in points if "G0" == elem[0] or "G1" == elem[0]]
x_max = max(points,key=itemgetter(1))[1] x_max = max(points,key=itemgetter(1))[1]
x_min = min(points,key=itemgetter(1))[1] x_min = min(points,key=itemgetter(1))[1]
y_max = max(points,key=itemgetter(2))[2] y_max = max(points,key=itemgetter(2))[2]
@ -155,12 +159,12 @@ def toolpath_info(points):
tool_changes = 0 tool_changes = 0
distances = [] distances = []
for point in points: for point in points:
if not ("G0" in point[0] or "G1" in point[0]): if not ("G0" == point[0] or "G1" == point[0]):
if "M6" in point[0]: if "M6" == point[0]:
tool_changes += 1 tool_changes += 1
distances.append(total_distance) distances.append(total_distance)
continue continue
if last_point: if last_point and "G1" == point[0]:
total_distance += math.hypot(point[1] - last_point[1], point[2] - last_point[2]) total_distance += math.hypot(point[1] - last_point[1], point[2] - last_point[2])
last_point = point last_point = point
distances.append(total_distance) distances.append(total_distance)
@ -178,6 +182,8 @@ def load_csv_file(csvfile, offset=(0,0)):
command = "G0" command = "G0"
commands = [("G28",0,0)] commands = [("G28",0,0)]
colors = [] colors = []
last_color = None
current_color = None
# load each point into commands list # load each point into commands list
for row in reader: for row in reader:
# empyt row # empyt row
@ -191,12 +197,15 @@ def load_csv_file(csvfile, offset=(0,0)):
elif 'COLOR' in row[1]: elif 'COLOR' in row[1]:
command = "M6" command = "M6"
if len(colors) > 0: if len(colors) > 0:
r, g, b = colors.pop(0) current_color = colors.pop(0)
else: else:
r = g = b = 0 current_color = (0,0,0)
commands.append([command, r, g, b]) # colors are truly different
if current_color != last_color:
commands.append([command, *current_color])
last_color = current_color
continue continue
elif 'TRIMM' in row[1]: elif 'TRIM' in row[1]:
command = "G12" command = "G12"
commands.append([command, ]) commands.append([command, ])
continue continue
@ -247,9 +256,9 @@ def load_gcode_file(f):
def save_gcode_file(f, commands): def save_gcode_file(f, commands):
for command in commands: for command in commands:
if "M6" in command[0]: if "M6" == command[0]:
command = "%s R%d G%d B%d\n" % (command[0], command[1], command[2], command[3]) command = "%s R%d G%d B%d\n" % (command[0], command[1], command[2], command[3])
elif "G0" in command[0] or "G1" in command[0]: elif "G0" == command[0] or "G1" == command[0]:
if len(command) > 3: if len(command) > 3:
command = "%s X%f Y%f F%f\n" % (command[0], command[1], command[2], command[3]) command = "%s X%f Y%f F%f\n" % (command[0], command[1], command[2], command[3])
else: else: