#!/usr/bin/python3 # # Copyright (c) 2022 Robert Bond # # Mdro 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 2 of the License, or # (at your option) any later version. # # Mdro 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. # # Manual DRO # Hook this one directly to the dro pins from __future__ import print_function import sys import os if sys.version_info[0] == 2: import Tkinter as tk else: import tkinter as tk import argparse import time import linuxcnc import hal root = tk.Tk() # Linux cnc interface class lc(): def __init__(self): try: self.s = linuxcnc.stat() self.s.poll() self.c = linuxcnc.command() self.h = hal.component("mdro") self.pins = ["axis."+str(p) for p in range(params["naxes"])] for pin in self.pins: self.h.newpin(pin, hal.HAL_FLOAT, hal.HAL_IN) self.indexes = ["index-enable."+str(p) for p in range(params["naxes"])] for pin in self.indexes: self.h.newpin(pin, hal.HAL_BIT, hal.HAL_IO) self.h.ready() if params["verbose"]: print("Linuxcnc interface up") except: print("mdro: Linuxcnc interface aborted") exit(1) def poll(self): self.s.poll() def get_pins(self): pins = [self.h[pin] for pin in self.pins] if params["very_verbose"]: print("pins:", pins) return pins def index_ready(self, row): pin_name = self.indexes[row] return self.h[pin_name] == 0 def set_index_enable(self, row): pin_name = self.indexes[row] if params["verbose"]: print(pin_name, "= 1") self.h[pin_name] = 1 # One of these for each DRO row class axis_row_gui(): def __init__(self, frame, row, text, entry_callback, index_callback): px = 10 self.row = row self.text = text self.value = tk.StringVar() if params["mm"] == 0: self.cur_format = params["inch_format"] else: self.cur_format = params["mm_format"] self.value.set(self.cur_format.format(0.0)) self.entry_callback = entry_callback self.index_callback = index_callback self.title = tk.Label(frame, justify=tk.RIGHT, anchor=tk.E, text=text, font=params["font1"]) self.title.grid(row=row, column=0, columnspan=1, sticky=tk.W) self.vlabel = tk.Label(frame, width=10, justify=tk.RIGHT, anchor=tk.E, textvariable=self.value, font=params["font1"]) self.vlabel.grid(row=row, column=1, columnspan=1, sticky=tk.W) self.zero = tk.Button(frame, text="0", font=params["font2"]) self.zero.bind("", lambda event: self.zero_up(event)) self.zero.grid(row=row, column=2, columnspan=1, padx=px, sticky=tk.W) self.half = tk.Button(frame, text="1/2", font=params["font2"]) self.half.bind("", lambda event: self.half_up(event)) self.half.grid(row=row, column=3, columnspan=1, padx=px, sticky=tk.W) self.entry = tk.Entry(frame, width=10, justify=tk.RIGHT, font=params["font1"], bg='light gray') self.entry.bind("", lambda event: self.enter_hit()) self.entry.bind("", lambda event: self.enter_clicked()) self.entry.grid(row=row, column=4, columnspan=1, sticky=tk.W, padx=px) self.index = tk.Button(frame, text="I", font=params["font2"]) self.index.bind("", lambda event: self.index_up(event)) self.index.grid(row=row, column=5, columnspan=1, padx=px, sticky=tk.W) self.disable_entry() def enter_hit(self): if params["verbose"]: print("enter_hit") try: v = float(self.entry.get()) except: print('\a') return self.entry_callback(self.row, v) self.entry.delete(0, tk.END) def enter_clicked(self): if params["verbose"]: print("enter_clicked") self.entry_callback(self.row, None) def zero_up(self, event): if params["verbose"]: print("zero_up") self.entry_callback(self.row, 0.0) def half_up(self, event): if params["verbose"]: print("half_up") self.entry_callback(self.row, float(self.value.get())/2.0) def index_up(self, event): if params["verbose"]: print("index_up") self.index_callback(self.row) def set_value(self, v): self.value.set(self.cur_format.format(v)) def kp_entry(self, key): if key == 'E': self.enter_hit() return if key == 'C': self.entry.delete(0, tk.END) return if key == '<': s = self.entry.get() self.entry.delete(0, tk.END) self.entry.insert(tk.END, s[:-1]) return self.entry.insert(tk.END, key) def disable_entry(self): self.entry.config(state=tk.DISABLED) def enable_entry(self): self.entry.config(state=tk.NORMAL) def disable_index(self): self.index.config(state=tk.DISABLED) def enable_index(self): self.index.config(state=tk.NORMAL) def update_units(self, units): if units == "inch": self.cur_format = params["inch_format"] else: self.cur_format = params["mm_format"] # The keypad class keypad_gui(): def __init__(self, frame, callback): self.kp_var = tk.StringVar() self.callback = callback rows = ((('7','7'),('8','8'),('9','9')), (('4','4'),('5','5'),('6','6')), (('1','1'),('2','2'),('3','3')), (('0','0'),('.','.'),('-','-')), (('C','C'),('\u232B','<'),('\u23CE','E'))) px = 5 for row, values in enumerate(rows): for col, e in enumerate(values): t, v = e rb = tk.Radiobutton(frame, text=t, variable=self.kp_var, value=v, width=4, indicatoron=0, command=lambda: self.kp_hit(), font=params["font1"]) rb.grid(row=row, column=col, padx=px) def kp_hit(self): key = self.kp_var.get() if params["verbose"]: print("kp_hit", key, "0x{:04x}".format(ord(key))) self.callback(key) self.kp_var.set("") class coord_systems(): def __init__(self, frame, callback): self.callback = callback self.rb_var = tk.IntVar() self.rb_var.set(1) if not params["preload"] is None: self.coord_sys = ['MCS', 'G54', 'G55', 'G56', 'G57'] else: self.coord_sys = ['MCS', 'CS1', 'CS2', 'CS3', 'CS4'] self.coords = [] for i in range(len(self.coord_sys)): self.coords.append([0.0]*params["naxes"]) if not params["preload"] is None: self.preload_cs() self.cur_idx = 1 self.cur_sys = self.coords[self.cur_idx] for row, cs in enumerate(self.coord_sys): rb = tk.Radiobutton(frame, text=cs, variable=self.rb_var, value=row, width=6, indicatoron=0, command=lambda: self.rb_hit(), font=params["font1"]) rb.grid(row=row, column=0, columnspan=1, padx=5) self.last_units_factor = 1.0 def rb_hit(self): self.cur_idx = self.rb_var.get() if params["verbose"]: print("rb_hit", self.coord_sys[self.cur_idx]) self.cur_sys = self.coords[self.cur_idx] self.callback(self.cur_idx) def update_units(self, units_factor): for i in range(len(self.coords)): for j in range(len(self.coords[i])): self.coords[i][j] /= self.last_units_factor self.coords[i][j] *= units_factor self.last_units_factor = units_factor def preload_cs(self): valid_axes = list("XYZABCUVW") for a in params["axes"]: if not (a in valid_axes or a.upper() in valid_axes): print('mdro: Axes must be one of "XYZABCUVW"') exit(1) if not os.path.isfile(params["preload"]): print("mdro: Could not find", params["preload"]) exit(1) number_to_load = (len(self.coord_sys) - 1)*20 max_idx = 5221 + number_to_load self.vc = [0.0] * number_to_load with open(params["preload"], 'r') as f: for line in f: line = line.strip() fields = line.split() try: idx = int(fields[0]) v = float(fields[1]) except: print("mdro: Invalid parameter file:", var_file) exit(1) if idx >= max_idx: break if idx >= 5221: self.vc[idx - 5221] = v axis_idx = {} for i, a in enumerate(valid_axes): axis_idx[a] = i axis_idx[a.lower()] = i for i in range(1, len(self.coord_sys)): for j, a in enumerate(params["axes"]): self.coords[i][j] = -(self.vc[(i - 1) * 20 + axis_idx[a]]) class main_gui(): def __init__(self, lcnc): self.lcnc = lcnc px = 5 py = 15 root.title("mdro") self.dro_frame = tk.Frame(root) self.axis_row = dict() self.last_row = None self.disp_inch = tk.IntVar() if params["mm"]: self.disp_inch.set(2) else: self.disp_inch.set(0) self.mm_adj = [1.0, 1.0/25.4, 25.4, 1.0] self.units_factor = 1.0 for row, name in enumerate(params["axes"]): self.axis_row[row] = axis_row_gui(self.dro_frame, row, name, self.entry_callback, self.index_callback) self.axis_row[row].enable_entry() self.dro_frame.grid(row=0, column=0, columnspan=2, padx=px, pady=py, sticky=tk.NW) self.keypad_frame = tk.Frame(root) self.keypad = keypad_gui(self.keypad_frame, self.keypad_callback) self.keypad_frame.grid(row=1, column=0, padx=px, pady=py, sticky=tk.NW) self.coord_frame = tk.Frame(root) self.coords = coord_systems(self.coord_frame, self.coord_callback) self.coord_frame.grid(row=1, column=1, padx=px, pady=py, sticky=tk.N) self.inch_frame = tk.Frame(root) self.inches = tk.Radiobutton(self.inch_frame, text="inch", variable=self.disp_inch, value=0, command=lambda: self.units_hit(), font=params["font1"]) self.inches.grid(row=0, column=0) self.inches = tk.Radiobutton(self.inch_frame, text="mm", variable=self.disp_inch, value=2, command=lambda: self.units_hit(), font=params["font1"]) self.inches.grid(row=0, column=1) self.inch_frame.grid(row=2, column=0, padx=px, pady=py, sticky=tk.NW) def units_hit(self): if params["verbose"]: print("units_hit", self.disp_inch.get()) self.units_factor = self.mm_adj[params["mm"] + self.disp_inch.get()] self.coords.update_units(self.units_factor) if self.disp_inch.get() == 0: new_units = "inch" else: new_units = "mm" for i in range(params["naxes"]): self.axis_row[i].update_units(new_units) def entry_callback(self, row, value): if params["verbose"]: print("Entry callback", row, value) if value is None: # just a click if not self.last_row is None: self.axis_row[self.last_row].entry.config(bg='light gray') self.axis_row[row].entry.config(bg='white') self.last_row = row return # Enter self.lcnc.poll() pins = self.lcnc.get_pins() # Can't change the mcs entry if self.coords.cur_idx != 0: pin = pins[row] * self.units_factor self.coords.cur_sys[row] = value - pin if not self.last_row is None: self.axis_row[self.last_row].entry.config(bg='light gray') self.last_row = None def coord_callback(self, coord_sys_idx): if params["verbose"]: print("coord_callback", coord_sys_idx) if coord_sys_idx == 0: for row in range(params["naxes"]): self.axis_row[row].disable_entry() else: for row in range(params["naxes"]): self.axis_row[row].enable_entry() def keypad_callback(self, key): if params["verbose"]: print("keypad_callback", key) if self.last_row is None: return self.axis_row[self.last_row].kp_entry(key) def index_callback(self, row): if params["verbose"]: print("main_index_callback", row) self.lcnc.poll() if self.lcnc.index_ready(row): self.lcnc.set_index_enable(row) else: if params["verbose"]: print("index seq not ready", row) def poll(self): self.lcnc.poll() pins = self.lcnc.get_pins() for i in range(len(pins)): pin = pins[i] * self.units_factor self.axis_row[i].set_value(pin + self.coords.cur_sys[i]) if self.lcnc.index_ready(i): self.axis_row[i].enable_index() else: self.axis_row[i].disable_index() def run_postgui(): # Run the postgui HAL files if called from DISPLAY section of INI file if params["ini"] is None or params["inifile"] is None: print("mdro inifile missing!") exit(1) if params["verbose"]: print("mdro INI file: ", params["ini"]) postgui_halfiles = params["inifile"].findall("HAL", "POSTGUI_HALFILE") or None if not postgui_halfiles is None: for f in postgui_halfiles: if params["verbose"]: halcmd_args = ["halcmd", "-V", "-i", args.ini, "-f", f] print("halcmd", halcmd_args) else: halcmd_args = ["halcmd", "-i", args.ini, "-f", f] res = os.spawnvp(os.P_WAIT, "halcmd", halcmd_args) if res: print("Error processing halfile:", f, "mdro exiting") exit(1) def call_polls(): global gui gui.poll() root.after(100, call_polls); def get_params(args): params = dict() params["verbose"] = False params["very_verbose"] = False if args.verbose > 0: params["verbose"] = True if args.verbose > 1: params["very_verbose"] = True params["is_display"] = not args.ini is None params["ini"] = args.ini if not args.ini is None: inifile = linuxcnc.ini(params["ini"]) params["inifile"] = inifile options = [ ["GEOMETRY", args.axes, "axes"], ["MDRO_VAR_FILE", args.load_cs, "preload"], ["POINT_SIZE", args.point_size, "point_size"], ["MM", args.mm, "mm"] ] if params["ini"] is None: for d, a, p in options: params[p] = a else: for d, a, p in options: params[p] = a dv = inifile.find("DISPLAY", d) or None if params["verbose"]: print("get_params", d, a, p, dv) if not dv is None: params[p] = dv params["axes"] = list(params["axes"]) params["naxes"] = len(params["axes"]) params["mm"] = int(params["mm"]) if params["mm"] < 0 or params["mm"] > 1: print("MM argument must be 0 or 1") exit(1) try: params["point_size"] = int(params["point_size"]) except: print("Point size must be an integer") params["font1"] = ("Helvetica", params["point_size"]) params["font2"] = ("Helvetica", int(params["point_size"] / 2)) params["inch_format"] = "{:.4f}" params["mm_format"] = "{:.2f}" return params if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--verbose', '-v', action='count', default=0, help='print debug info') parser.add_argument('--point_size', '-p', dest='point_size', type=int, default=20, help='font point size, default: 20') parser.add_argument('--mm', '-m', action='store_const', const=1, default=0, help='dro values in mm') parser.add_argument("--load_cs", "-l", type=str, help="load g5x coordinate system") parser.add_argument("--ini", "-ini", type=str, help="INI file name") parser.add_argument("axes", nargs='?', type=str, default='XYZ', help="Axes (example: XYZ)") args = parser.parse_args() params = get_params(args) lcnc = lc() gui = main_gui(lcnc) if params["is_display"]: run_postgui() root.after(20, call_polls); root.mainloop()