Files
linuxcncAllWasm/lib/python/qtvcp/widgets/dro_widget.py
2026-08-20 06:17:39 +08:00

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()