import logging import time from tkinter import Button, Label, Variable, IntVar, Canvas, Frame, Listbox, Entry, Radiobutton, Tk, constants, LEFT from tkinter import filedialog as fd import os from grbl_streamer import GrblStreamer import settings class touchCNC: def __init__(self, root): self.root = root # GUI Main self.stick_var = None self.stick_var_disp = 'NSWE' self.buttonsize_x = 5 self.buttonsize_y = 3 self.buttonsize_y_s = 1 self.pady_var = 3 self.file_list = [] self.list_items = Variable(value=self.file_list) self.increments = 0 self.BORDER = 2 self.feedspeed = None self.states = {'M3': '0', 'M8': '0', 'M6': '0', 'G10': '0', '32' :'0'} # self.spindle, Coolant, Toolchange self.dict_GCODE = {'G': '0', 'X': '0', 'Y': '0', 'Z': '0', 'I': '0', 'J': '0', 'F': '0' } # GUI Color Scheme self.attention = '#ED217C' self.special = '#EC058E' self.loaded = '#90E39A' self.cooling = '#86BBD8' self.toolchange = '#ADE25D' self.secondary = '#B9A44C' self.standard = '#6DB1BF' #F5F5F5' self.feed = self.secondary self.transport = '#FA7921' # Classic Scheme attention = 'red' loaded = 'green' cooling = 'blue' toolchange = 'yellow' standard = '#17223B' feed = '#283B67' self.increments = IntVar() self.movement = Frame(root, relief='ridge', bd=self.BORDER, padx=10, pady=10) self.left = Button(root, text="-X", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.jogWrite('X', '-1', self.increments), bd=self.BORDER, bg=self.standard) self.right = Button(root, text="+X", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.jogWrite('X', '1', self.increments), bd=self.BORDER, bg=self.standard) self.up = Button(root, text="+Y", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.jogWrite('Y', '1', self.increments), bd=self.BORDER, bg=self.standard) self.down = Button(root, text="-Y", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.jogWrite('Y', '-1', self.increments), bd=self.BORDER, bg=self.standard) self.z_up = Button(root, text="+Z", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.jogWrite('Z', '1', self.increments), bd=self.BORDER, bg=self.standard) self.z_down = Button(root, text="-Z", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.jogWrite('Z', '-1', self.increments), bd=self.BORDER, bg=self.standard) self.zero_x = Button(root, text="zero X", width=self.buttonsize_x, height=self.buttonsize_y_s, command=lambda: self.directWrite('G10 P0 L20 X0'), bd=self.BORDER, bg=self.secondary) self.zero_y = Button(root, text="zero Y", width=self.buttonsize_x, height=self.buttonsize_y_s, command=lambda: self.directWrite('G10 P0 L20 Y0'), bd=self.BORDER, bg=self.secondary) self.zero_z = Button(root, text="zero Z", width=self.buttonsize_x, height=self.buttonsize_y_s, command=lambda: self.directWrite('G10 P0 L20 Z0'), bd=self.BORDER, bg=self.secondary) self.zero_all = Button(root, text="zeroAll", width=self.buttonsize_x, height=self.buttonsize_y_s, command=lambda: self.latchWrite('G10'), bd=self.BORDER, bg=self.special) self.setzero = Button(root, text="SetPOS", width=self.buttonsize_x, height=self.buttonsize_y_s, command=lambda: self.directWrite('G28.1'), bd=self.BORDER, bg= self.standard) self.gozero = Button(root, text="GoPOS", width=self.buttonsize_x, height=self.buttonsize_y_s, command=lambda: self.directWrite('G28'), bd=self.BORDER , bg= self.attention) self.connect_ser = Button(root, text="Cnnct", width=self.buttonsize_x, height=self.buttonsize_y, command=self.grblConnect2, bg=self.standard, bd=self.BORDER) self.discon_ser = Button(root, text="Dsconct", width=self.buttonsize_x, height=self.buttonsize_y, command= self.grblClose, bd=self.BORDER, bg=self.standard) self.unlock = Button(root, text="Unlock", width=self.buttonsize_x, height=1, command=self.grblUnlock, bd=self.BORDER) self.start = Button(root, text="START", width=self.buttonsize_x, height=self.buttonsize_y, bg=self.attention, command=self.grblWrite, bd=self.BORDER) self.stop = Button(root, text="STOP", width=self.buttonsize_x, height=self.buttonsize_y, bd=self.BORDER, bg=self.transport, command=self.grblStop) self.pause = Button(root, text="PAUSE", width=self.buttonsize_x, height=self.buttonsize_y, bd=self.BORDER, bg=self.transport, command=self.grblPause) self.resume = Button(root, text="RESUME", width=self.buttonsize_x, height=self.buttonsize_y, bd=self.BORDER, bg=self.transport, command=self.grblResume) self.fopen = Button(root, text="OPEN", width=self.buttonsize_x, height=self.buttonsize_y, bg=self.standard, fg='black', command=self.openGCODE, bd=self.BORDER) self.fopendir = Button(root, text="DIR", width=self.buttonsize_x, height=self.buttonsize_y, bg=self.standard, fg='black', command=self.openDir, bd=self.BORDER) self.spindle = Button(root, text="Spindle", width=self.buttonsize_x, height=self.buttonsize_y, bg=self.standard, command=lambda: self.latchWrite('M3')) self.coolant = Button(root, text="Coolant", width=self.buttonsize_x, height=self.buttonsize_y, bg=self.standard, command=lambda: self.latchWrite('M8')) self.tool = Button(root, text="Tool", width=self.buttonsize_x, height=self.buttonsize_y, bg=self.standard, command=lambda: self.directWrite('G10')) self.macro = Button(root, text="Laser", width=self.buttonsize_x, height=self.buttonsize_y, bg=self.standard, command=lambda: self.latchWrite('32')) #self.directWrite(' G91 G0 X10 Y10 Z50 F1000')) self.inc1 = Button(root, text="Inc 1%", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.feed_over_write(1), bg=self.feed) self.inc10 = Button(root, text="Inc 10%", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.feed_over_write(10), bg=self.feed) self.dec1 = Button(root, text="Dec 1%", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.feed_over_write(-1), bg=self.feed) self.dec10 = Button(root, text="Dec 10%", width=self.buttonsize_x, height=self.buttonsize_y, command=lambda: self.feed_over_write(-10), bg=self.feed) self.reset = Button(root, text=" 9: self.feedspeed = data[10] elif eventstring == "on_read": if data[0] == '$32=1': self.macro.config(background=self.attention) elif data[0] == '$32=0': self.macro.config(background=self.loaded) #elif eventstring == "on_processed_command": # pass elif eventstring == "on_line_sent": pass else: self.terminal_recv.config(text=eventstring) self.terminal_recv_content.config(text=data) import time import logging def grblConnect2(self, baudrate=115200, max_retries=5, retry_interval=3): retry_count = 0 locations = settings.portlist # Configure logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) if not grbl.connected: for device in locations: if retry_count < max_retries: try: logger.info(f"Attempting to connect to {device}") grbl.cnect(path=device, baudrate=baudrate) break except Exception as e: logger.error(f"Failed to connect to {device}: {e}") self.terminal_recv_content.config(text=f"Failed to connect to {device}: {e}") retry_count += 1 time.sleep(retry_interval) logger.warning(f"Failed to connect after {max_retries} attempts.") finally: time.sleep(3) grbl.setup_logging() self.connect_ser.config(bg=self.loaded) #rbl.request_settings() logger.info(f"Connection successful to {device}") self.terminal_recv_content.config(text=f"Connection successful to {device}") grbl.connected = True grbl.poll_start() self.terminal_recv_content.config(text=f"State: {grbl.connected}") #grbl.set_feed_override(True) def grblClose(self): grbl.softreset() print(grbl.connected) grbl.disconnect() self.connect_ser.config(bg=self.secondary) def displayWorkPosition(self, pos: list): #print("update", pos) self.show_ctrl_x_w.config(text = pos[0]) self.show_ctrl_y_w.config(text = pos[1]) self.show_ctrl_z_w.config(text = pos[2]) #self.root.after(1000, self.getPosition) def jogWrite(self, axis, cmd, scale): DECIMAL = [0.1, 1, 10, 100] scale = self.increments.get() MOVE = int(cmd) * DECIMAL[scale - 1] grbl_command = ('$J=G91' + 'G21' + axis + str(MOVE) + 'F1000') # print(grbl_command) $J=G91G21X10F185 grbl.send_immediately(grbl_command) def directWrite(self, cmd): grbl.send_immediately(cmd) def feed_over_write(self, change: int): pass new_feed = self.feedspeed / 100 * change print(new_feed) grbl.request_feed(new_feed) def latchWrite(self, CMD): if self.states[CMD] == '0': self.states[CMD] = '1' self.update_button_color(CMD, True) else: self.states[CMD] = '0' self.update_button_color(CMD, False) grbl_command = self.get_grbl_command(CMD) grbl.send_immediately(grbl_command) def update_button_color(self, CMD, is_active): if CMD == 'M3': self.spindle.config(bg=self.attention if is_active else self.loaded) elif CMD == 'M6': self.tool.config(bg=self.toolchange if is_active else self.standard) elif CMD == 'G10': self.zero_all.config(bg=self.loaded if is_active else self.attention) elif CMD == '32': self.macro.config(bg=self.loaded if is_active else self.attention) elif CMD == 'M8': self.coolant.config(bg=self.cooling if is_active else self.standard) def get_grbl_command(self, CMD): if CMD == 'M3': return settings.spindle_on if self.states['M3'] == '1' else settings.spindle_off elif CMD == 'M8': return settings.cooling_on if self.states[CMD] == '1' else settings.cooling_off elif CMD == 'G10': return settings.toolchange elif CMD == '32': return '$32=0' if self.states['32'] == '1' else '$32=1' else: return CMD def overrideCMD(self, cmd): pass #grbl. def openGCODE(self): filetypes = (('GCODE', '*.nc'), ('All files', '*.*')) if not self.file_list: GCODE = fd.askopenfilename(title='Open a file', initialdir=settings.basepath, filetypes=filetypes) else: GCODE = self.load_gcode_from_listbox() if GCODE: grbl.abort() grbl.job_new() self.fopen.config(bg=self.loaded) extracted = self.extract_GCODE(GCODE) draw = DrawonTable(self.mill_table) draw.drawgridTable() draw.setGCODE(extracted) draw.draw_GCODE(extracted) grbl.load_file(GCODE) else: self.fopen.config(bg=self.secondary) def get_filenames(self, base_path): filenames = [] full_path_list = [] # Use os.listdir to get the list of files and directories in the base path entries = os.listdir(base_path) if entries: for entry in entries: # Use os.path.join to create the full path of the entry full_path = os.path.join(base_path, entry) # Check if the entry is a file (not a directory) if os.path.isfile(full_path): filenames.append(full_path) print(filenames) else: filenames = ["Such Empty"] return filenames def openDir(self): self.file_list = [] self.path_list = [] directory = fd.askdirectory(title='Open a Folder', initialdir=settings.basepath) allowed_extensions = {'nc', 'GCODE'} # Use a set for efficient membership testing if directory: path = self.get_filenames(directory) self.files.delete(0, constants.END) for file in path: # Check if the file has an allowed extension if any(file.lower().endswith(ext) for ext in allowed_extensions): self.path_list.append(file) file = file.split('/')[-1] self.file_list.append(file) self.files.insert("end", file) # Add the filename to the Listbox else: print("Please select Folder") self.terminal_recv_content.config(text="Please select Folder") #print(self.file_list) def load_gcode_from_listbox(self): """Loads selected file path from the listbox selection for passing it to the loaded indirectly""" selected_indices = self.files.curselection() if selected_indices: selected_index = selected_indices[0] selected_item = self.path_list[selected_index] print("Selected item:", selected_item) else: print("No item selected") return selected_item def grblWrite(self): grbl.job_run() def grblStop(self): grbl.job_halt() def grblPause(self): grbl.hold() self.pause.config(bg='red') def grblResume(self): grbl.resume() self.pause.config(bg=self.transport) def grblUnlock(self): grbl.killalarm() def extract_GCODE(self, gcode_path: str): # Aufschlüsseln der enthaltenen Koordinaten in ein per Schlüssel zugängiges Dictionary print(gcode_path) with open(gcode_path, 'r') as gcode: list_dict_GCODE = [] for line in gcode: lines = line.split() # Elemente trennen und in Liste konvertieren for command in lines: # print (l) if 'G' in command: self.dict_GCODE['G'] = command.replace('G', '') # Wert einfügen und gleichzeitig G CODE befehl entfernen if 'X' in command: self.dict_GCODE['X'] = command.replace('X', '') if 'Y' in command: self.dict_GCODE['Y'] = command.replace('Y', '') if 'Z' in command: self.dict_GCODE['Z'] = command.replace('Z', '') if 'I' in command and not 'ZMIN': self.dict_GCODE['I'] = command.replace('I', '') if 'J' in command: self.dict_GCODE['J'] = command.replace('J', '') if 'F' in command and not 'Fusion': self.dict_GCODE['F'] = command.replace('F', '') # print(dict_GCODE) list_dict_GCODE.append( self.dict_GCODE.copy()) # Copy notwendig da es sich nur um einen "Pointer" handelt der immer auf die zuletzt aktualisierte dict Zeile zeigt. #print(list_dict_GCODE) return list_dict_GCODE class DrawonTable: def __init__(self, mill_table: object): self.mill_table = mill_table self.gcode: list = None self.cursor_pos = None def setPos(self,pos): if pos != None: self.cursor_pos = pos def setGCODE(self, gcode: list): if gcode != None: self.gcode = gcode def clearTable(self): self.mill_table.delete('all') def drawToolCursor(self): id = self.mill_table.create_text(50 + float(self.cursor_pos[0]), 342 - float(self.cursor_pos[1]), text='V', fill = 'red', font=("Arial", 16)) return id def deleteCursor(self, id): if id != None: #print("deleted") self.mill_table.delete(id) def draw_GCODE(self, glist): # Zeichnen des GCodes zur Beurteilung des Bauraums self.drawgridTable() for i in range(0, len(glist) - 1): x_y_current = 50 + float(glist[i]['X']), 350 - float(glist[i]['Y']) x_y_next = 50 + float(glist[i + 1]['X']), 350 - float(glist[i + 1]['Y']) self.mill_table.create_line(x_y_current, x_y_next) def get_coordinates(self, point): x_str = point.get('X', '0') y_str = point.get('Y', '0') x = 50 + float(x_str[1:]) if x_str and x_str[0] == 'X' else 50 y = 350 - float(y_str[1:]) if y_str and y_str[0] == 'Y' else 350 return x, y def drawgridTable(self): self.mill_table.delete('all') self.mill_table.create_rectangle(50, 50, 350, 350, fill='white') self.mill_table.create_text(200, 25, text=settings.table_text) for x in range(50, 350, 50): self.mill_table.create_text(x, 400 - x, text=x - 50) for x in range(0, 400, 50): for y in range(0, 400, 50): gitter_x = self.mill_table.create_line(x, 0, x, 400) gitter_y = self.mill_table.create_line(0, y, 400, y) if __name__ == "__main__": root = Tk() root.title('touchCNC') root.geometry(settings.resolution) root.grid_propagate(True) root.resizable(False, False) # 17203b root.attributes('-fullscreen', settings.set_fullscreen) root.tk_setPalette(background='#4B4A67', foreground='black', activeBackground='#F99417', activeForeground='lightgrey') app = touchCNC(root) grbl = GrblStreamer(app.gui_callback) grbl.hash_state_requested = True grbl.gcode_parser_state_requested = True root.mainloop()