diff --git a/control_app/app.py b/control_app/app.py index 92da911..0fff1b1 100644 --- a/control_app/app.py +++ b/control_app/app.py @@ -1,6 +1,6 @@ #!/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 import tkinter.ttk as ttk 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 import copy, re, math, time, pickle +#import control_serial_mockup as serial import control_serial as serial class ControlAppGUI: @@ -88,6 +89,15 @@ class ControlAppGUI: self.stopButton = Button(tab1, text="STOP", command=self.StopAll, state=DISABLED) 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) Label(progressFrame, text="Tool changes: ", bd=1).grid(row=0,column=0) 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.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal)) 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) 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.') def Settings(self): tl = Toplevel(root) @@ -267,7 +277,7 @@ class ControlAppGUI: data = {"workAreaSize": self.workAreaSize} pickle.dump(data, f) except Exception as e: - print ("error while saving settings:", str(e)) + print ("Error while saving settings:", str(e)) def ToggleConnect(self): if self.isConnected: @@ -294,6 +304,7 @@ class ControlAppGUI: rectangle = toolpath_border_points(self.commands[1:]) for point in rectangle: serial.queue_command("G0 X%f Y%f F5000\n" % point) + def ToggleStart(self): if self.isJobPaused: serial.queue.clear() @@ -308,51 +319,83 @@ class ControlAppGUI: self.canvas.clear() startInstructionIndex = 0 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 - commandsCount = len(self.commands) - # 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.QueueCommandsBlock(startInstructionIndex) 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): newState = NORMAL if enabled else DISABLED for b in self.navigationButtons: @@ -416,14 +459,17 @@ class ControlAppGUI: self.currentColor = 'black' self.toolPointsLabel.config(text="%d/%d" % (self.currentToolPoint, self.toolPointsTotal)) 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.status.config(text="Job finished") timeTaken = time.time() - self.start # non blocking popup messagebox if messagePopup: tl = Toplevel(root) + # this pop-up is always on top and other windows are deactivated + tl.attributes('-topmost', 'true') tl.title("Job finished") + tl.grab_set() frame = Frame(tl) 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) @@ -476,6 +522,12 @@ class ControlAppGUI: if self.isJobRunning: return 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): if self.isConnected: def TimerCallback(response): @@ -490,28 +542,28 @@ class ControlAppGUI: def CleanUp(self): serial.close_serial() - def ToolChangePopup(self, newColor = "black"): + def PausePopup(self, newColor = "black", trim = False): tl = Toplevel(root) + tl.attributes('-topmost', 'true') + tl.grab_set() + 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.grid() - canvas = Canvas(frame, width=100, height=130) - canvas.grid(row=1, column=0) - #imgvar = PhotoImage(file="pyrocket.png") - #canvas.create_image(50,70, image=imgvar) - #canvas.image = imgvar - - 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) + canvas = Canvas(frame, width=64, height=64) + canvas.grid(row=2, column=0) + canvas.create_rectangle(0, 0, 65, 65, fill=newColor) + msgbody = Label(frame, text="There is the moment to %s. Resume the current job after change." % msg) + msgbody.grid(row=1, column=0, sticky=N) 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() my_gui = ControlAppGUI(root) diff --git a/control_app/control_serial_mockup.py b/control_app/control_serial_mockup.py index bb93e8f..3938a9f 100644 --- a/control_app/control_serial_mockup.py +++ b/control_app/control_serial_mockup.py @@ -60,7 +60,7 @@ def send_serial(msg, responseCallback = None): global last_pos response = "ok\n" 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: regex_result = gcode_regexX.search(msg) if regex_result: @@ -113,4 +113,4 @@ class SendingThread(threading.Thread): queue.task_done() def terminate(self): - self.running = False \ No newline at end of file + self.running = False diff --git a/control_app/path_preview.py b/control_app/path_preview.py index 837d5ec..0021df3 100644 --- a/control_app/path_preview.py +++ b/control_app/path_preview.py @@ -83,10 +83,14 @@ class ResizingCanvas(Canvas): current_color = "black" color = current_color for point in points[1:]: - if "G0" in point[0]: + if "G0" == point[0]: color = "snow2" - elif "M6" in point[0]: + elif "M6" == point[0]: 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 coord = (last_point[1], last_point[2], point[1], point[2]) last_point = point @@ -99,7 +103,7 @@ class ResizingCanvas(Canvas): def translate_toolpath(points, translate=(0,0)): #new_points = copy.deepcopy(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 point[1] += translate[0] point[2] += translate[1] @@ -107,7 +111,7 @@ def translate_toolpath(points, translate=(0,0)): def rotate_toolpath(points, origin, theta): 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 point[1], point[2] = rotate(point[1:3], origin, theta) return points @@ -127,21 +131,21 @@ def rotate(point, origin, angle): def reflect_toolpath(points, d): 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 point[1] = 2*d - point[1] return points def scale_toolpath(points, f): 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 point[1] += point[1]*f point[2] += point[2]*f return 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_min = min(points,key=itemgetter(1))[1] y_max = max(points,key=itemgetter(2))[2] @@ -155,12 +159,12 @@ def toolpath_info(points): tool_changes = 0 distances = [] for point in points: - if not ("G0" in point[0] or "G1" in point[0]): - if "M6" in point[0]: + if not ("G0" == point[0] or "G1" == point[0]): + if "M6" == point[0]: tool_changes += 1 distances.append(total_distance) 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]) last_point = point distances.append(total_distance) @@ -178,6 +182,8 @@ def load_csv_file(csvfile, offset=(0,0)): command = "G0" commands = [("G28",0,0)] colors = [] + last_color = None + current_color = None # load each point into commands list for row in reader: # empyt row @@ -191,12 +197,15 @@ def load_csv_file(csvfile, offset=(0,0)): elif 'COLOR' in row[1]: command = "M6" if len(colors) > 0: - r, g, b = colors.pop(0) + current_color = colors.pop(0) else: - r = g = b = 0 - commands.append([command, r, g, b]) + current_color = (0,0,0) + # colors are truly different + if current_color != last_color: + commands.append([command, *current_color]) + last_color = current_color continue - elif 'TRIMM' in row[1]: + elif 'TRIM' in row[1]: command = "G12" commands.append([command, ]) continue @@ -247,9 +256,9 @@ def load_gcode_file(f): def save_gcode_file(f, 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]) - elif "G0" in command[0] or "G1" in command[0]: + elif "G0" == command[0] or "G1" == command[0]: if len(command) > 3: command = "%s X%f Y%f F%f\n" % (command[0], command[1], command[2], command[3]) else: