#!/usr/bin/env python3 # Gscreen a GUI for linuxcnc cnc controller # Chris Morley copyright 2012 # # 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 2 of the License, or # (at your option) any later version. # # This 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. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ # Gscreen is made for running LinuxCNC CNC machines # Gscreen was built with touchscreens in mind though a mouse works too. # a keyboard is necessary for editing gcode # Gscreen is, at it's heart, a gladevcp program though loaded in a non standard way. # one can also use a second monitor to display a second glade panel # this would probably be most useful for user's custom status widgets. # you would need to calibrate your touchscreen to just work on a single screen """ import sys,os,subprocess def _print_help(): print("""\nGscreen is a customizable operator screen for LinuxCNC based on PyGTK3 / Glade.\n It is usually loaded from LinuxCNC's INI file under the [DISPLAY] section. eg. DISPLAY = gscreen\n Options: --INI.................Designates the configuration file path for LinuxCNC -c....................Loads an optional skin for Gscreen -q....................Quiet logging - only log errors and critical -i....................Info logging -d....................Debug logging -v....................Verbose logging -F....................Prints names of internal functions to standard output -FD...................Prints names and documentation of internal functions to standard output -h or --help..........Show this help text\n If q, i, v, or d are not specified then logging will default to Warning logging. """) sys.exit(0) for num,temp in enumerate(sys.argv): if temp == '-h' or temp == '--help' or len(sys.argv) == 1: _print_help() # Set up the base logger # We have do do this before importing other modules because on import # they set up their own loggers as children of the base logger. # If log_file is none, logger.py will attempt to find the log file specified in # INI [DISPLAY] LOG_FILE, failing that it will log to $HOME/.log # Note: In all other modules it is best to use the `__name__` attribute # to ensure we get a logger with the correct hierarchy. # Ex: LOG = logger.getLogger(__name__) # we set the log level early so the imported modules get the right level # The order is: VERBOSE, DEBUG, INFO, WARNING, ERROR, CRITICAL. from qtvcp import logger LOG = logger.initBaseLogger('GScreen', log_file=None, log_level=logger.WARNING) if '-d' in sys.argv: # Log level defaults to WARNING, so set lower if in debug mode logger.setGlobalLevel(logger.DEBUG) LOG.debug('DEBUGGING logging on') elif '-i' in sys.argv: # Log level defaults to WARNING, so set lower if in info mode logger.setGlobalLevel(logger.INFO) LOG.info('INFO logging on') elif '-v' in sys.argv: # Log level defaults to WARNING, so set lowest if in verbose mode logger.setGlobalLevel(logger.VERBOSE) LOG.verbose('VERBOSE logging on') # Log level defaults to WARNING, so set higher if in quiet mode (error and critical) elif '-q' in sys.argv: logger.setGlobalLevel(logger.ERROR) import gi gi.require_version("Gtk","3.0") gi.require_version("Gdk","3.0") from gi.repository import Gtk, Gdk, GLib from gi.repository import Pango as pango import hal import errno import gladevcp.makepins import traceback import atexit import time from time import strftime,localtime import hal_glib #-------------------------------------------------------- # limit number of times err msgs are displayed excepthook_msg_ct = 0 excepthook_msg_ct_max = 10 update_spindle_bar_error_ct = 0 update_spindle_bar_error_ct_max = 3 #-------------------------------------------------------- # try to add a notify system so messages use the # nice integrated pop-ups # Ubuntu kinda wrecks this be not following the # standard - you can't set how long the message stays up for. # I suggest fixing this with a PPA off the net # https://launchpad.net/~leolik/+archive/leolik?field.series_filter=lucid NOTIFY_AVAILABLE = False try: gi.require_version('Notify', '0.7') from gi.repository import Notify if Notify.init("Gscreen"): NOTIFY_AVAILABLE = True LOG.info(_("Desktop notifications are available")) else: LOG.warning(_("Desktop notifications are not available")) except: LOG.warning(_("There was a problem initializing the notification module")) # try to add ability for audio feedback to user. try: gi.require_version('Gst', '1.0') from gi.repository import Gst _AUDIO_AVAILABLE = True LOG.info(_("Audio alerts are available!")) except: _AUDIO_AVAILABLE = False LOG.warning(_("No audio alerts are available - Is gir1.2-gstreamer-1.0 package installed?")) # try to add ability to show an embedded terminal for debugging try: gi.require_version('Vte', '2.91') from gi.repository import Vte _TERMINAL_AVAILABLE = True except: _TERMINAL_AVAILABLE = False LOG.warning("Could not import Vte terminal - is gir1.2-vte-2.91 package installed?") # BASE is the absolute path to linuxcnc base # libdir is the path to Gscreen python files # datadir is where the standard GLADE files are # imagedir is for icons # themedir is path to system's GTK3 theme folder BASE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "..")) libdir = os.path.join(BASE, "lib", "python") datadir = os.path.join(BASE, "share", "linuxcnc") imagedir = os.path.join(BASE, "share","gscreen","images") SKINPATH = os.path.join(BASE, "share","gscreen","skins") sys.path.insert(0, libdir) themedir = "/usr/share/themes" userthemedir = os.path.join(os.path.expanduser("~"), ".themes") # set soundsdir based on distribution soundsdir = '/usr/share/sounds/freedesktop/stereo' if not os.path.exists(soundsdir): try: import distro if 'mint' in distro.id().lower(): soundsdir = '/usr/share/sounds/LinuxMint/stereo' if not os.path.exists(soundsdir): LOG.error(f'Audio player - Mint sound file directory not found: {soundsdir}') soundsdir = None else: LOG.error(f'Audio player - Could not find sounds directory for {distro.id()} distro') soundsdir = None except: LOG.error(f'Audio player - Could not find sounds directory - Is python3-distro installed?') soundsdir = None xmlname = os.path.join(datadir,"gscreen.glade") xmlname2 = os.path.join(datadir,"gscreen2.glade") ALERT_ICON = os.path.join(imagedir,"applet-critical.png") INFO_ICON = os.path.join(imagedir,"std_info.gif") # internationalization and localization import locale, gettext # path to TCL for external programs eg. halshow try: TCLPATH = os.environ['LINUXCNC_TCL_DIR'] except: pass # path to the configuration the user requested # used to see if the is local GLADE files to use try: CONFIGPATH = os.environ['CONFIG_DIR'] except: pass import linuxcnc from gscreen import emc_interface from gscreen import mdi from gscreen import preferences from gscreen import keybindings # this is for hiding/showing the pointer when using a touch screen VISIBLE = Gdk.Cursor(Gdk.CursorType.ARROW) INVISIBLE = Gdk.Cursor(Gdk.CursorType.BLANK_CURSOR) # Throws up a dialog with debug info when an error is encountered def excepthook(exc_type, exc_obj, exc_tb): try: w = app.widgets.window1 except KeyboardInterrupt: sys.exit(0) except NameError: w = None lines = traceback.format_exception(exc_type, exc_obj, exc_tb) text = '' for n in lines: text += n e = Gscreen.get_exception_list(None, text) global excepthook_msg_ct, excepthook_msg_ct_max excepthook_msg_ct += 1 if excepthook_msg_ct < excepthook_msg_ct_max: gap = ' ' * 18 LOG.error(f"Exception #{excepthook_msg_ct}\n{gap}{gap.join(e)}") if excepthook_msg_ct < 1: m = Gtk.MessageDialog( parent = w, message_type = Gtk.MessageType.ERROR, buttons = Gtk.ButtonsType.OK, text = ("Gscreen encountered an error. The following " "information may be useful in troubleshooting:\n\n") + "".join(lines), modal=True, destroy_with_parent=True ) m.show() m.run() m.destroy() sys.excepthook = excepthook # constants _X = 0;_Y = 1;_Z = 2;_A = 3;_B = 4;_C = 5;_U = 6;_V = 7;_W = 8 _ABS = 0;_REL = 1;_DTG = 2 _SPINDLE_INPUT = 1;_PERCENT_INPUT = 2;_VELOCITY_INPUT = 3;_DEGREE_INPUT = 4 # the player class does the work of playing the audio hints # http://pygstdocs.berlios.de/pygst-tutorial/introduction.html class Player: def __init__(self): Gst.init(None) #Element playbin automatic plays any file self.player = Gst.ElementFactory.make("playbin", "player") #Enable message bus to check for errors in the pipeline bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) self.loop = GLib.MainLoop() def run(self): self.player.set_state(Gst.State.PLAYING) self.loop.run() def set_sound(self,file): #Set the uri to the file self.player.set_property("uri", "file://" + file) def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: #file ended, stop self.player.set_state(Gst.State.NULL) self.loop.quit() elif t == Gst.MessageType.ERROR: #Error occurred, print and stop self.player.set_state(Gst.State.NULL) err, debug = message.parse_error() LOG.error("{} {}".format(err, debug)) self.loop.quit() # a class for holding the glade widgets rather then searching for them each time class Widgets: def __init__(self, xml): self._xml = xml def __getattr__(self, attr): r = self._xml.get_object(attr) if r is None: raise AttributeError(_("No widget '{}'").format(attr)) return r def __getitem__(self, attr): r = self._xml.get_object(attr) if r is None: raise IndexError(_("No widget '{}'").format(attr)) return r # a class for holding data # here we initialize the data class Data: def __init__(self): # constants for mode idenity self._MAN = 0 self._MDI = 1 self._AUTO = 2 self._JOG = 3 self._MM = 1 self._IMPERIAL = 0 # paths included to give access to handler files self.SKINPATH = SKINPATH self.CONFIGPATH = CONFIGPATH self.BASEPATH = BASE self.audio_available = False self.use_screen2 = False self.theme_name = "Follow System Theme" self.abs_textcolor = "" self.rel_textcolor = "" self.dtg_textcolor = "" self.err_textcolor = "" self.window_geometry = "" self.window_max = "" self.axis_list = [] self.rotary_joints = False self.active_axis_buttons = [(None,None)] # axis letter,axis number self.abs_color = (0, 65535, 0) self.rel_color = (65535, 0, 0) self.dtg_color = (0, 0, 65535) self.highlight_color = (65535,65535,65535) self.highlight_major = False self.display_order = (_REL,_DTG,_ABS) self.mode_order = (self._MAN,self._MDI,self._AUTO) self.mode_labels = [_("Manual Mode"),_("MDI Mode"),_("Auto Mode")] self.IPR_mode = False self.plot_view = ("p","x","y","y2","z","z2") self.task_mode = 0 self.active_gcodes = [] self.active_mcodes = [] for letter in ('x','y','z','a','b','c','u','v','w'): self['%s_abs'%letter] = 0.0 self['%s_rel'%letter] = 0.0 self['%s_dtg'%letter] = 0.0 self['%s_is_homed'%letter] = False self.spindle_request_rpm = 0 self.spindle_dir = 0 self.spindle_speed = 0 self.spindle_start_rpm = 300 self.spindle_preset = 300 self.active_spindle_command = "" # spindle command setting self.active_feed_command = "" # feed command setting self.system = 1 self.estopped = True self.dro_units = self._IMPERIAL self.machine_units = self._IMPERIAL self.tool_in_spindle = 0 self.flood = False self.mist = False self.machine_on = False self.or_limits = False self.op_stop = False self.block_del = False self.all_homed = False self.jog_rate = 15 self.jog_rate_inc = 1 self.jog_rate_max = 60 self.jog_increments = [] self.current_jogincr_index = 0 self.angular_jog_adjustment_flag = False self.angular_jog_increments = [] self.angular_jog_rate = 1800 self.angular_jog_rate_inc = 60 self.angular_jog_rate_max = 7200 self.current_angular_jogincr_index = 0 self.feed_override = 1.0 self.feed_override_inc = .05 self.feed_override_max = 2.0 self.rapid_override = 1.0 self.rapid_override_inc = .05 self.rapid_override_max = 1.0 self.spindle_override = 1.0 self.spindle_override_inc = .05 self.spindle_override_max = 1.2 self.spindle_override_min = .50 self.maxvelocity = 1 self.velocity_override = 1.0 self.velocity_override_inc = .05 self.edit_mode = False self.full_graphics = False self.graphic_move_inc = 20 self.plot_hidden = False self.file = "" self.file_lines = 0 self.line = 0 self.last_line = 0 self.motion_line = 0 self.id = 0 self.dtg = 0.0 self.show_dtg = False self.velocity = 0.0 self.delay = 0.0 self.preppedtool = None self.lathe_mode = False self.diameter_mode = True self.tooleditor = "" self.tooltable = "" self.alert_sound = "" self.error_sound = "" self.ob = None self.index_tool_dialog = None self.keyboard_dialog = None self.preset_spindle_dialog = None self.spindle_control_dialog = None self.entry_dialog = None self.restart_dialog = None self.key_event_last = None,0 def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value) # trampoline and load_handlers are used for custom displays class Trampoline(object): def __init__(self,methods): self.methods = methods def __call__(self, *a, **kw): for m in self.methods: m(*a, **kw) def load_handlers(usermod,halcomp,builder,useropts,gscreen): hdl_func = 'get_handlers' mod = object = None def add_handler(method, f): if method in handlers: handlers[method].append(f) else: handlers[method] = [f] handlers = {} for u in usermod: (directory,filename) = os.path.split(u) (basename,extension) = os.path.splitext(filename) if directory == '': directory = '.' if directory not in sys.path: sys.path.insert(0,directory) LOG.info(_("Adding import dir {}").format(directory)) try: mod = __import__(basename) except ImportError as msg: LOG.error(_("Module '{}' skipped - import error: {}").format(basename, msg)) continue LOG.info(_("Module '{}' imported OK").format(mod.__name__)) try: # look for functions for temp in ("periodic","connect_signals","initialize_widgets"): h = getattr(mod,temp,None) if h and callable(h): LOG.info(_("Module '{}': '{}' function found").format(mod.__name__, temp)) # look for 'get_handlers' function h = getattr(mod,hdl_func,None) if h and callable(h): LOG.info(_("Module '{}': '{}' function found").format(mod.__name__, hdl_func)) objlist = h(halcomp,builder,useropts,gscreen) else: # the module has no get_handlers() callable. # in this case we permit any callable except class Objects in the module to register as handler LOG.debug(_("Module '{}': no '{}' function - registering only functions as callbacks").format(mod.__name__,hdl_func)) objlist = [mod] # extract callback candidates for object in objlist: LOG.debug(_("Registering handlers in module '{}' object '{}'").format(mod.__name__, object)) if isinstance(object, dict): methods = list(dict.items()) else: methods = [(n, getattr(object, n, None)) for n in dir(object)] for method,f in methods: if method.startswith('_'): continue if callable(f): LOG.debug(("Register callback '{}' in {}").format(method, basename)) add_handler(method, f) except Exception as e: LOG.error(_("Trouble looking for handlers in '{}': {}").format(basename, e)) traceback.print_exc() # Wrap lists in Trampoline, unwrap single functions for n,v in list(handlers.items()): if len(v) == 1: handlers[n] = v[0] else: handlers[n] = Trampoline(v) return handlers,mod,object # ok here is the Gscreen class # there are also three other files: # mdi.py for mdi commands (which include non-obvious mdi commands done in maual mode) # preference.py for keeping track of stored user preferences # emc_interface.py which does most of the commands and status of linuxcnc # keep in mind some of the gladeVCP widgets send-commands-to/monitor linuxcnc also class Gscreen: def __init__(self): global xmlname global xmlname2 self.skinname = "gscreen" (progdir, progname) = os.path.split(sys.argv[0]) # linuxcnc adds -ini to display name and optparse # can't understand that, so we do it manually for num,temp in enumerate(sys.argv): if temp == '-c': try: LOG.info(_("Skin name = {}").format(sys.argv[num+1])) self.skinname = sys.argv[num+1] except: pass if temp == '-F': self._print_functions() sys.exit(0) if temp == '-FD': self._print_functions(True) sys.exit(0) try: self.inipath = sys.argv[2] except: LOG.error(_("INI file path missing from Gscreen launch command")) _print_help() sys.exit(0) # check for a local translation folder locallocale = os.path.join(CONFIGPATH,"locale") if os.path.exists(locallocale): LOCALEDIR = locallocale domain = self.skinname LOG.info(_("Custom locale name = {}").format(LOCALEDIR, self.skinname)) else: locallocale = os.path.join(SKINPATH,"%s/locale"%self.skinname) if os.path.exists(locallocale): LOCALEDIR = locallocale domain = self.skinname LOG.info(_("Skin locale name = {}").format(LOCALEDIR, self.skinname)) else: LOCALEDIR = os.path.join(BASE, "share", "locale") domain = "linuxcnc" locale.setlocale(locale.LC_ALL, '') locale.bindtextdomain(domain, LOCALEDIR) gettext.install(domain, localedir=LOCALEDIR) gettext.bindtextdomain(domain, LOCALEDIR) # main screen localglade = os.path.join(CONFIGPATH,"%s.glade"%self.skinname) if os.path.exists(localglade): LOG.info(_("Using custom glade file from {}").format(localglade)) xmlname = localglade else: localglade = os.path.join(SKINPATH,"%s/%s.glade"%(self.skinname,self.skinname)) if os.path.exists(localglade): LOG.info(_(" Using skin glade file from {}").format(localglade)) xmlname = localglade else: LOG.info(_("Using stock glade file from: {}").format(xmlname2)) try: self.xml = Gtk.Builder() self.xml.set_translation_domain(domain) # for locale translations self.xml.add_from_file(xmlname) # this is a fix for themeing - it sets the widgets style name to # the widget id name. You can over ride it later with: # self.widgets..set_name('