513 lines
19 KiB
Python
513 lines
19 KiB
Python
#!/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()
|