#!/usr/bin/env python3 # qtVcp Widget - DRO label widget # This widgets displays linuxcnc axis position information. # # Copyright (c) 2017 Chris Morley # # 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. import linuxcnc from qtpy import QtCore from qtpy.QtWidgets import QMenu, QAction from qtpy.QtGui import QIcon from qtvcp.widgets.simple_widgets import ScaledLabel from qtvcp.widgets.widget_baseclass import _HalWidgetBase from qtvcp.core import Status, Info, Action from qtvcp import logger # Instantiate the libraries with global reference # INFO holds INI file details # STATUS gives us status messages from linuxcnc # LOG is for running code logging STATUS = Status() INFO = Info() ACTION = Action() LOG = logger.getLogger(__name__) # Force the log level for this module # LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL class DROLabel(ScaledLabel, _HalWidgetBase): def __init__(self, parent=None): super(DROLabel, self).__init__(parent) self._mode = False self._joint_type = 1 self.diameter = False self.reference_type = 0 # axis index of 9 axis self.axis_index = 0 # linuxcnc joint number self._jointNum = 0 self.display_units_mm = 0 self.metric_text_template = '%10.3f' self.imperial_text_template = '%9.4f' self.angular_text_template = '%9.2f' self.setText('--------------') self.allow_reference_change_requests = True self.follow_m7m8_mode = True self.force_diameter = False self.force_radius = False self._scale = 1 self._user = 0 self._text =' -000.0000' # menu stuff self.dialog_code = 'CALCULATOR' self._showGoto = False self._showZero = False self._showSet = False self._showLast = False self._showDivide = False # for stylesheet reading self._isHomed = False def mousePressEvent(self, event): menu = QMenu(self) state = (STATUS.machine_is_on() and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED)) if self._showZero: self.zeroButton = QAction(QIcon('exit24.png'), 'Zero', self) self.zeroButton.triggered.connect(self.Zero) self.zeroButton.setEnabled(state) menu.addAction(self.zeroButton) if self._showSet: self.setButton = QAction(QIcon('exit24.png'), 'Set', self) self.setButton.triggered.connect(self.SetOrigin) self.setButton.setEnabled(state) menu.addAction(self.setButton) if self._showDivide: self.divideButton = QAction(QIcon('exit24.png'), 'Divide By 2', self) self.divideButton.triggered.connect(self.Divide) self.divideButton.setEnabled(state) menu.addAction(self.divideButton) if self._showLast: self.lastButton = QAction(QIcon('exit24.png'), 'Set To Last', self) self.lastButton.triggered.connect(self.Last) self.lastButton.setEnabled(state) menu.addAction(self.lastButton) if self._showGoto: text = 'Go To G53 Origin in {}'.format(INFO.GET_NAME_FROM_JOINT.get(self.axis_index)) self.goToG53Button = QAction(QIcon('exit24.png'), text, self) self.goToG53Button.triggered.connect(self.goToG53) self.goToG53Button.setEnabled(state) menu.addAction(self.goToG53Button) text = 'Go To G5x Origin in {}'.format(INFO.GET_NAME_FROM_JOINT.get(self.axis_index)) self.goToG5xButton = QAction(QIcon('exit24.png'), text, self) self.goToG5xButton.triggered.connect(self.goToG5x) self.goToG5xButton.setEnabled(state) menu.addAction(self.goToG5xButton) menu.popup(event.globalPos()) def _hal_init(self): super(DROLabel, self)._hal_init() STATUS.connect('homed', lambda w,d: self._home_status_polish(int(d), True)) STATUS.connect('unhomed', lambda w,d: self._home_status_polish(int(d), False)) # get position update from STATUS every 100 ms if self.axis_index == 10: STATUS.connect('current-z-rotation', self.update_rotation) else: self._jointNum = INFO.GET_JOINT_NUM_FROM_AXIS_INDEX.get(self.axis_index) if self._jointNum is None: LOG.debug('axis number {} not found in available-axis to joint conversion dict {} of widget: {}'.format(self.axis_index, INFO.GET_JOINT_NUM_FROM_AXIS_INDEX, self.objectName())) self._jointNum = 0 STATUS.connect('motion-mode-changed',self.motion_mode) STATUS.connect('current-position', self.update) STATUS.connect('metric-mode-changed', self._switch_units) if self.follow_m7m8_mode: STATUS.connect('diameter-mode', self._switch_modes) if self.allow_reference_change_requests: STATUS.connect('dro-reference-change-request', self._status_reference_change) try: self._joint_type = INFO.JOINT_TYPE_INT[self._jointNum] except: self._joint_type = 1 if self._joint_type == linuxcnc.ANGULAR: self._current_text_template = self.angular_text_template elif self.display_units_mm: self._current_text_template = self.metric_text_template else: self._current_text_template = self.imperial_text_template STATUS.connect('general',self.return_value) # update ishomed property # polish widget so stylesheet sees the property change # some stylesheets color the text on home/unhome def _home_status_polish(self, d, state): if d == self._jointNum or (self.axis_index==10 and d==1): self.setProperty('isHomed', state) self.style().unpolish(self) self.style().polish(self) def motion_mode(self, w, mode): if mode == linuxcnc.TRAJ_MODE_COORD: pass # Joint mode elif mode == linuxcnc.TRAJ_MODE_FREE: self._mode = False # axis elif mode == linuxcnc.TRAJ_MODE_TELEOP: self._mode = True @QtCore.Slot(int) @QtCore.Slot(float) def update_user(self, data): self._user = data def update_rotation(self, widget, rotation): degtmpl = lambda s: self.angular_text_template % s self.setText(degtmpl(rotation)) def update(self, widget, absolute, relative, dtg, joint): if self.display_units_mm != INFO.MACHINE_IS_METRIC: absolute = INFO.convert_units_9(absolute) relative = INFO.convert_units_9(relative) dtg = INFO.convert_units_9(dtg) tmpl = lambda s: self._current_text_template % s try: if self.reference_type == 0: if not self._mode and STATUS.stat.kinematics_type != linuxcnc.KINEMATICS_IDENTITY: self.setText(tmpl(joint[self._jointNum])) else: self.setText(tmpl(absolute[self.axis_index]*self._scale)) elif self.reference_type == 1: self.setText(tmpl(relative[self.axis_index]*self._scale)) elif self.reference_type == 2: self.setText(tmpl(dtg[self.axis_index]*self._scale)) elif self.reference_type == 10: self.setText(tmpl(self._user*self._scale)) except: pass def _status_reference_change(self,w ,value): self.reference_type = value def _switch_units(self, widget, data): self.display_units_mm = data self.update_units() def update_units(self): if self._joint_type == linuxcnc.ANGULAR: self._current_text_template = self.angular_text_template elif self.display_units_mm: self._current_text_template = self.metric_text_template else: self._current_text_template = self.imperial_text_template def _switch_modes(self, w, mode): self.diameter = mode # only joint 0 (X) can use diameter mode if mode and self.axis_index == 0: self._scale = 2.0 else: self._scale = 1 def set_to_inch(self): self.display_units_mm = 0 if self._joint_type == linuxcnc.ANGULAR: self._current_text_template = self.angular_text_template else: self._current_text_template = self.imperial_text_template def set_to_mm(self): self.display_units_mm = 1 if self._joint_type == linuxcnc.ANGULAR: self._current_text_template = self.angular_text_template else: self._current_text_template = self.metric_text_template def set_to_diameter(self): self.diameter = True if self.axis_index == 0: self._scale = 2.0 def set_to_radius(self): self.diameter = False self._scale = 1.0 # index = index of 9 axis def set_joint_type(self, index): # convert to linxcnc joint number self._jointNum = INFO.GET_JOINT_NUM_FROM_AXIS_INDEX.get(index) if self._jointNum is None: self._joint_type = 1 else: self._joint_type = INFO.JOINT_TYPE_INT[self._jointNum] self. update_units() ####################### # menu option code ####################### def Zero(self): pos = self.get_current_position() if not pos is None: axis = "XYZABCUVW"[self.axis_index] ACTION.SET_AXIS_ORIGIN(axis, 0) STATUS.emit('update-machine-log', 'Zeroed Axis %s' % axis, 'TIME,SUCCESS') def SetOrigin(self): pos = self.get_current_position() if not pos is None: axis = "XYZABCUVW"[self.axis_index] mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(), 'AXIS':axis, 'CURRENT':pos, 'TITLE':'Set %s Origin'% axis, 'GEONAME':'DROWidgetDialog_{}'.format(self.dialog_code), 'AXIS':axis} STATUS.emit('dialog-request', mess) LOG.debug('message sent:{}'.format (mess)) # process the STATUS return message def return_value(self, w, message): num = message['RETURN'] code = bool(message.get('ID') == '%s__'% self.objectName()) name = bool(message.get('NAME') == self.dialog_code) if code and name and num is not None: LOG.debug('message return:{}'.format (message)) axis = message.get('AXIS') ACTION.SET_AXIS_ORIGIN(axis, num) STATUS.emit('update-machine-log', 'Set Origin of Axis %s to %f' %(axis, num), 'TIME,SUCCESS') def Divide(self): pos = self.get_current_position() if not pos is None: axis = "XYZABCUVW"[self.axis_index] try: x = pos/2.0 ACTION.SET_AXIS_ORIGIN(axis, x) text = 'Divided Axis %s in Half - %f'% (axis, x) STATUS.emit('update-machine-log', text, 'TIME,SUCCESS') except ZeroDivisionError: pass def Last(self): pos = self.get_current_position() if not pos is None: axis = "XYZABCUVW"[self.axis_index] last = ACTION.GET_LAST_RECORDED_ORIGIN(axis) ACTION.SET_AXIS_ORIGIN(axis, last) text = 'Reset Axis %s from %f to Last Value: %f' %(axis, pos, last) STATUS.emit('update-machine-log', text, 'TIME,SUCCESS') def get_current_position(self): if STATUS.is_joint_mode(): return None p,r,d = STATUS.get_position() if self.display_units_mm != INFO.MACHINE_IS_METRIC: r = INFO.convert_units_9(r) return r[self.axis_index] def goToG53(self): ACTION.CALL_MDI('G53 G0 {}0'.format(INFO.GET_NAME_FROM_JOINT.get(self.axis_index))) def goToG5x(self): ACTION.CALL_MDI('G0 {}0'.format(INFO.GET_NAME_FROM_JOINT.get(self.axis_index))) ######################################################################### # This is how designer can interact with our widget properties. # designer will show the Property properties in the editor # it will use the get set and reset calls to do those actions # # _toggle_properties makes it so we can only select one option ######################################################################## def _toggle_properties(self, picked): data = ('always_display_diameter','always_display_radius', 'display_as_per_m7m8') for i in data: if not i == picked: self[i] = False # property getter/setters def set_force_diameter(self, data): self.force_diameter = data if data: self.set_to_diameter() self._toggle_properties('always_display_diameter') def get_force_diameter(self): return self.force_diameter def reset_force_diameter(self): self.force_diameter = False always_display_diameter = QtCore.Property(bool, get_force_diameter, set_force_diameter, reset_force_diameter) def set_force_radius(self, data): self.force_radius = data if data: self.set_to_radius() self._toggle_properties('always_display_radius') def get_force_radius(self): return self.force_radius def reset_force_radius(self): self.force_radius = False always_display_radius = QtCore.Property(bool, get_force_radius, set_force_radius, reset_force_radius) def set_follow_m7m8_mode(self, data): self.follow_m7m8_mode = data if data: self._toggle_properties('display_as_per_m7m8') def get_follow_m7m8_mode(self): return self.follow_m7m8_mode def reset_follow_m7m8_mode(self): self.follow_m7m8_mode = True display_as_per_m7m8 = QtCore.Property(bool, get_follow_m7m8_mode, set_follow_m7m8_mode, reset_follow_m7m8_mode) def set_follow_reference(self, data): self.allow_reference_change_requests = data def get_follow_reference(self): return self.allow_reference_change_requests def reset_follow_reference(self): self.allow_reference_change_requests = True follow_reference_changes = QtCore.Property(bool, get_follow_reference, set_follow_reference, reset_follow_reference) # JOINT Number TODO this is actually joint index of the 9 axes # rather then linuxcnc's idea of joint number def setjoint_number(self, data): if data >10: data = 10 if data < 0: data = 0 self.axis_index = data if data < 10: self.set_joint_type(data) def getjoint_number(self): return self.axis_index def resetjoint_number(self): self.axis_index = 0 Qjoint_number = QtCore.Property(int, getjoint_number, setjoint_number, resetjoint_number) # user system Number def setreference_type(self, data): self.reference_type = data def getreference_type(self): return self.reference_type def resetreference_type(self): self.reference_type = 0 Qreference_type = QtCore.Property(int, getreference_type, setreference_type, resetreference_type) def setmetrictemplate(self, data): self.metric_text_template = data self.update_units() def getmetrictemplate(self): return self.metric_text_template def resetmetrictemplate(self): self.metric_text_template = '%10.3f' metric_template = QtCore.Property(str, getmetrictemplate, setmetrictemplate, resetmetrictemplate) def setimperialtexttemplate(self, data): self.imperial_text_template = data self.update_units() def getimperialtexttemplate(self): return self.imperial_text_template def resetimperialtexttemplate(self): self.imperial_text_template = '%9.4f' imperial_template = QtCore.Property(str, getimperialtexttemplate, setimperialtexttemplate, resetimperialtexttemplate) def setangulartexttemplate(self, data): self.angular_text_template = data self.update_units() def getangulartexttemplate(self): return self.angular_text_template def resetangulartexttemplate(self): self.angular_text_template = '%9.2f' angular_template = QtCore.Property(str, getangulartexttemplate, setangulartexttemplate, resetangulartexttemplate) def setisHomed(self, data): self._isHomed = data def getisHomed(self): return self._isHomed isHomed = QtCore.Property(bool, getisHomed, setisHomed) #################### ## menu properties #################### def set_dialog_code(self, data): self.dialog_code = data def get_dialog_code(self): return self.dialog_code def reset_dialog_code(self): self.dialog_code = 'CALCULATOR' dialogName = QtCore.Property(str, get_dialog_code, set_dialog_code, reset_dialog_code) def set_showSet(self, data): self._showSet = data def get_showSet(self): return self._showSet def reset_showSet(self): self._showSet = False showSetOrigin = QtCore.Property(bool, get_showSet, set_showSet, reset_showSet) def set_showZero(self, data): self._showZero = data def get_showZero(self): return self._showZero def reset_showZero(self): self._showZero = False showZeroOrigin = QtCore.Property(bool, get_showZero, set_showZero, reset_showZero) def set_showGoto(self, data): self._showGoto = data def get_showGoto(self): return self._showGoto def reset_showGoto(self): self._showGoto = False showGotoOrigin = QtCore.Property(bool, get_showGoto, set_showGoto, reset_showGoto) def set_showLast(self, data): self._showLast = data def get_showLast(self): return self._showLast def reset_showLast(self): self._showLast = False showLast = QtCore.Property(bool, get_showLast, set_showLast, reset_showLast) def set_showDivide(self, data): self._showDivide = data def get_showDivide(self): return self._showDivide def reset_showDivide(self): self._showDivide = False showDivide = QtCore.Property(bool, get_showDivide, set_showDivide, reset_showDivide) ############################## # required class boiler code # ############################## def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value) # for testing without editor: def main(): import sys from qtpy.QtWidgets import QApplication app = QApplication(sys.argv) widget = DROLabel() widget.show() sys.exit(app.exec()) if __name__ == "__main__": main()