390 lines
18 KiB
Python
390 lines
18 KiB
Python
|
|
'''
|
|
qtplasmac_sim_handler.py
|
|
|
|
Copyright (C) 2020 - 2024 Phillip A Carter
|
|
Copyright (C) 2020 - 2024 Gregory D Carl
|
|
|
|
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.
|
|
'''
|
|
|
|
import os
|
|
import sys
|
|
import hal
|
|
from subprocess import run as RUN
|
|
from qtpy import QtCore
|
|
from qtpy.QtGui import QIcon
|
|
from qtpy.QtWidgets import QMessageBox
|
|
from qtvcp.core import Info
|
|
from qtvcp.lib.preferences import Access
|
|
|
|
INFO = Info()
|
|
|
|
|
|
class HandlerClass:
|
|
def __init__(self, halcomp, widgets, paths):
|
|
self.hal = halcomp
|
|
self.w = widgets
|
|
self.paths = paths
|
|
self.iniFile = INFO.INI
|
|
self.w.setWindowFlags(QtCore.Qt.CustomizeWindowHint |
|
|
QtCore.Qt.WindowTitleHint |
|
|
QtCore.Qt.WindowStaysOnTopHint)
|
|
self.machineName = self.iniFile.getstring('EMC', 'MACHINE', fallback="<unknown>")
|
|
self.styleFile = f'{self.paths.CONFIGPATH}/qtplasmac_sim.qss'
|
|
self.set_style()
|
|
self.set_estop()
|
|
|
|
def initialized__(self):
|
|
self.w.setWindowTitle('QtPlasmaC Sim')
|
|
self.iconPath = 'share/icons/hicolor/scalable/apps/linuxcnc_alt/linuxcncicon_plasma.svg'
|
|
appPath = os.path.realpath(os.path.dirname(sys.argv[0]))
|
|
self.iconBase = '/usr' if appPath == '/usr/bin' else appPath.replace('/bin', '/debian/extras/usr')
|
|
self.w.setWindowIcon(QIcon(os.path.join(self.iconBase, self.iconPath)))
|
|
self.arcVoltsOffsetPin = self.hal.newpin('arc_voltage_offset-f', hal.HAL_FLOAT, hal.HAL_OUT)
|
|
self.breakPin = self.hal.newpin('sensor_breakaway', hal.HAL_BIT, hal.HAL_OUT)
|
|
self.floatPin = self.hal.newpin('sensor_float', hal.HAL_BIT, hal.HAL_OUT)
|
|
self.materialPin = self.hal.newpin('material_height', hal.HAL_FLOAT, hal.HAL_IN)
|
|
self.ohmicPin = self.hal.newpin('sensor_ohmic', hal.HAL_BIT, hal.HAL_OUT)
|
|
self.torchPin = self.hal.newpin('torch_on', hal.HAL_BIT, hal.HAL_IN)
|
|
self.statePin = self.hal.newpin('state', hal.HAL_S32, hal.HAL_IN)
|
|
self.stylePin = self.hal.newpin('style_update', hal.HAL_BIT, hal.HAL_IN)
|
|
self.zPosPin = self.hal.newpin('z_position', hal.HAL_FLOAT, hal.HAL_IN)
|
|
simStepconf = False
|
|
for sig in hal.get_info_signals():
|
|
if sig['NAME'] == 'Zjoint-pos-fb':
|
|
simStepconf = True
|
|
break
|
|
if simStepconf:
|
|
RUN(['halcmd', 'net', 'Zjoint-pos-fb', 'qtplasmac_sim.z_position'])
|
|
else:
|
|
RUN(['halcmd', 'net', 'plasmac:axis-position', 'qtplasmac_sim.z_position'])
|
|
RUN(['halcmd', 'net', 'plasmac:state', 'qtplasmac_sim.state'])
|
|
RUN(['halcmd', 'net', 'qtplasmac:sim_style_update', 'qtplasmac.sim_style_update','qtplasmac_sim.style_update'])
|
|
self.stylePin.value_changed.connect(self.update_style)
|
|
self.torchPin.value_changed.connect(self.torch_changed)
|
|
self.zPosPin.value_changed.connect(lambda v: self.z_position_changed(v))
|
|
self.statePin.value_changed.connect(lambda v: self.plasmac_state_changed(v))
|
|
self.w.arc_voltage_offset.valueChanged.connect(lambda v: self.arc_volts_offset_changed(v))
|
|
self.w.sensor_flt.pressed.connect(self.float_pressed)
|
|
self.w.sensor_ohm.pressed.connect(self.ohmic_pressed)
|
|
self.w.sensor_brk.pressed.connect(self.break_pressed)
|
|
self.w.arc_ok.clicked.connect(self.arc_ok_clicked)
|
|
self.w.estop.pressed.connect(self.estop_pressed)
|
|
self.w.auto_flt.pressed.connect(self.auto_float_pressed)
|
|
self.w.auto_flt.setChecked(True)
|
|
self.w.auto_ohm.pressed.connect(self.auto_ohmic_pressed)
|
|
self.fTimer = QtCore.QTimer()
|
|
self.fTimer.setInterval(500)
|
|
self.fTimer.setSingleShot(True)
|
|
self.fTimer.timeout.connect(self.float_timer_done)
|
|
self.oTimer = QtCore.QTimer()
|
|
self.oTimer.setInterval(500)
|
|
self.oTimer.setSingleShot(True)
|
|
self.oTimer.timeout.connect(self.ohmic_timer_done)
|
|
self.bTimer = QtCore.QTimer()
|
|
self.bTimer.setInterval(500)
|
|
self.bTimer.setSingleShot(True)
|
|
self.bTimer.timeout.connect(self.break_timer_done)
|
|
self.w.help.pressed.connect(self.help_pressed)
|
|
mode = hal.get_value('plasmac.mode')
|
|
self.set_mode(mode)
|
|
hal.set_p('estop_or.in0', '1')
|
|
self.height = 5 if hal.get_value('halui.machine.units-per-mm') == 1 else 0.2
|
|
self.materialPin.set(self.height)
|
|
self.w.estop.setStyleSheet(f'color: {self.backColor}; background: {self.estopColor}; border: solid {self.estopColor}')
|
|
self.floatLatched = False
|
|
self.ohmicLatched = False
|
|
|
|
def set_estop(self):
|
|
if self.prefs.getpref('Estop type', 0, int, 'GUI_OPTIONS') == 2:
|
|
RUN(['halcmd', 'net', 'sim:estop-1-raw', 'iocontrol.0.user-enable-out', 'estop_not_1.in'])
|
|
RUN(['halcmd', 'net', 'sim:estop-1-in', 'estop_not_1.out', 'estop_or.in1'])
|
|
|
|
def set_style(self):
|
|
self.prefs = Access(os.path.join(self.paths.CONFIGPATH, self.machineName + '.prefs'))
|
|
self.foreColor = self.prefs.getpref('Foreground', '', str, 'COLOR_OPTIONS')
|
|
self.fore1Color = self.prefs.getpref('Highlight', '', str, 'COLOR_OPTIONS')
|
|
self.backColor = self.prefs.getpref('Background', '', str, 'COLOR_OPTIONS')
|
|
self.back1Color = self.prefs.getpref('Background Alt', '', str, 'COLOR_OPTIONS')
|
|
self.disabledColor = self.prefs.getpref('Disabled', '', str, 'COLOR_OPTIONS')
|
|
self.estopColor = self.prefs.getpref('Estop', '', str, 'COLOR_OPTIONS')
|
|
with open(self.styleFile, 'w') as outFile:
|
|
outFile.write(
|
|
'/****** DEFAULT ************/\n'
|
|
f'* {{\n'
|
|
f' color: {self.foreColor};\n'
|
|
f' background: {self.backColor};\n'
|
|
f' font: 10pt DejaVuSans }}\n'
|
|
'\n/****** BUTTONS ************/\n'
|
|
f'QPushButton {{\n'
|
|
f' color: {self.foreColor};\n'
|
|
f' background: {self.backColor};\n'
|
|
f' border: 1px solid {self.foreColor};\n'
|
|
f' border-radius: 4px }}\n'
|
|
f'\nQPushButton:checked {{\n'
|
|
f' color: {self.backColor};\n'
|
|
f' background: {self.fore1Color} }}\n'
|
|
'\n#move_down:pressed,\n'
|
|
f'#move_up:pressed {{\n'
|
|
f'color: {self.backColor};\n'
|
|
f'background: {self.fore1Color} }}\n'
|
|
'\n/****** SLIDER ************/\n'
|
|
f'QSlider::groove:horizontal {{\n'
|
|
' background: gray;\n'
|
|
' border-radius: 4px;\n'
|
|
f' height: 20px }}\n'
|
|
f'\nQSlider::handle:horizontal {{\n'
|
|
f' background: {self.foreColor};\n'
|
|
f' border: 0px solid {self.foreColor};\n'
|
|
' border-radius: 4px;\n'
|
|
f' width: 24px }}\n'
|
|
'\nQSlider::add-page:horizontal:disabled,'
|
|
'\nQSlider::sub-page:horizontal:disabled,'
|
|
'\nQSlider::handle:horizontal:disabled,'
|
|
f'\nQSlider::groove:horizontal:disabled {{\n'
|
|
f' color: {self.disabledColor};\n'
|
|
f' background: {self.back1Color};\n'
|
|
f' border: 1px solid {self.disabledColor};\n'
|
|
f' border-radius: 4px }}\n'
|
|
'\nQSlider::add-page:horizontal,'
|
|
f'\nQSlider::sub-page:horizontal {{\n'
|
|
f' background: {self.back1Color};\n'
|
|
f' border: 1px solid {self.back1Color};\n'
|
|
f' border-radius: 4px }}\n'
|
|
'\n/****** LINE ***************/\n'
|
|
'\n#arc_ok_line,'
|
|
'\n#arc_voltage_line,'
|
|
'\n#line,'
|
|
'\n#mode_line,'
|
|
f'\n#sensor_line {{\n'
|
|
f' background: {self.foreColor} }}\n'
|
|
'\n/****** CHECKBOX ***********/\n'
|
|
f'QCheckBox {{\n'
|
|
f' spacing: 20px }}\n'
|
|
f'\nQCheckBox::indicator {{\n'
|
|
f' border: 1px solid {self.foreColor};\n'
|
|
' border-radius: 4px;\n'
|
|
' width: 20px;\n'
|
|
f' height: 20px }}\n'
|
|
'\nQCheckBox::indicator:pressed,'
|
|
'\nQCheckBox::indicator:checked,'
|
|
f'\nQCheckBox::indicator:checked:pressed {{\n'
|
|
f' background: {self.fore1Color} }}\n'
|
|
)
|
|
|
|
def update_style(self):
|
|
self.set_style()
|
|
self.w.setStyleSheet('')
|
|
with open(self.styleFile, 'r') as set_style:
|
|
self.w.setStyleSheet(set_style.read())
|
|
estop_color = self.estopColor if hal.get_value('estop_or.in0') == 1 else self.foreColor
|
|
self.w.estop.setStyleSheet(f'color: {self.backColor}; background: {estop_color}; border: solid {estop_color}')
|
|
conditions = {
|
|
self.w.arc_ok: self.w.arc_ok.isChecked(),
|
|
self.w.sensor_brk: self.breakPin.get() == 1,
|
|
self.w.sensor_flt: self.floatPin.get() == 1,
|
|
self.w.sensor_ohm: self.ohmicPin.get() == 1 }
|
|
for button, condition in conditions.items():
|
|
if condition:
|
|
button.setStyleSheet(f'color: {self.backColor}; background: {self.fore1Color}')
|
|
|
|
def arc_ok_clicked(self):
|
|
if self.w.arc_ok.isChecked():
|
|
self.w.arc_ok.setStyleSheet(f'color: {self.backColor}; background: {self.fore1Color}')
|
|
else:
|
|
self.w.arc_ok.setStyleSheet(f'color: {self.foreColor}; background: {self.backColor}')
|
|
|
|
def float_timer_done(self):
|
|
if self.below_material():
|
|
return
|
|
if self.w.sensor_flt.isDown():
|
|
self.floatLatched = True
|
|
return
|
|
self.float_reset()
|
|
|
|
def ohmic_timer_done(self):
|
|
if self.below_material():
|
|
return
|
|
if self.w.sensor_ohm.isDown():
|
|
self.ohmicLatched = True
|
|
return
|
|
self.ohmic_reset()
|
|
|
|
def break_timer_done(self):
|
|
if not self.w.sensor_brk.isDown():
|
|
self.breakPin.set(0)
|
|
self.w.sensor_brk.setStyleSheet(f'color: {self.foreColor}; background: {self.backColor}')
|
|
|
|
def float_pressed(self):
|
|
if self.fTimer.isActive():
|
|
self.fTimer.stop() # stop timer so next click can start it again
|
|
self.float_set()
|
|
else:
|
|
if self.floatPin.get():
|
|
self.fTimer.stop()
|
|
self.float_reset()
|
|
self.floatLatched = False
|
|
else:
|
|
self.float_set()
|
|
self.fTimer.start()
|
|
|
|
def ohmic_pressed(self):
|
|
if self.oTimer.isActive():
|
|
self.oTimer.stop() # stop timer so next click can start it again
|
|
self.ohmic_set()
|
|
else:
|
|
if self.ohmicPin.get():
|
|
self.oTimer.stop()
|
|
self.ohmic_reset()
|
|
self.ohmicLatched = False
|
|
else:
|
|
self.ohmic_set()
|
|
self.oTimer.start()
|
|
|
|
def float_set(self):
|
|
self.floatPin.set(1)
|
|
self.w.sensor_flt.setStyleSheet(f'color: {self.backColor}; background: {self.fore1Color}')
|
|
|
|
def float_reset(self):
|
|
self.floatPin.set(0)
|
|
self.w.sensor_flt.setStyleSheet(f'color: {self.foreColor}; background: {self.backColor}')
|
|
|
|
def ohmic_set(self):
|
|
self.ohmicPin.set(1)
|
|
self.w.sensor_ohm.setStyleSheet(f'color: {self.backColor}; background: {self.fore1Color}')
|
|
|
|
def ohmic_reset(self):
|
|
self.ohmicPin.set(0)
|
|
self.w.sensor_ohm.setStyleSheet(f'color: {self.foreColor}; background: {self.backColor}')
|
|
|
|
def auto_float_pressed(self):
|
|
if self.w.auto_ohm.isChecked:
|
|
self.w.auto_ohm.setChecked(False)
|
|
|
|
def auto_ohmic_pressed(self):
|
|
if self.w.auto_flt.isChecked:
|
|
self.w.auto_flt.setChecked(False)
|
|
|
|
def break_pressed(self):
|
|
if self.bTimer.isActive():
|
|
self.bTimer.stop() # stop timer so next click can start it again
|
|
self.breakPin.set(1)
|
|
self.w.sensor_brk.setStyleSheet(f'color: {self.backColor}; background: {self.fore1Color}')
|
|
else:
|
|
if self.breakPin.get():
|
|
self.bTimer.stop()
|
|
self.breakPin.set(0)
|
|
self.w.sensor_brk.setStyleSheet(f'color: {self.foreColor}; background: {self.backColor}')
|
|
else:
|
|
self.breakPin.set(1)
|
|
self.w.sensor_brk.setStyleSheet(f'color: {self.backColor}; background: {self.fore1Color}')
|
|
self.bTimer.start()
|
|
|
|
def estop_pressed(self):
|
|
if hal.get_value('estop_or.in0') == 0:
|
|
hal.set_p('estop_or.in0', '1')
|
|
self.w.estop.setStyleSheet(f'color: {self.backColor}; background: {self.estopColor}; border: solid {self.estopColor}')
|
|
else:
|
|
hal.set_p('estop_or.in0', '0')
|
|
self.w.estop.setStyleSheet(f'color: {self.backColor}; background: {self.foreColor}')
|
|
|
|
def set_mode(self, mode):
|
|
mode0 = [self.w.sensor_line,
|
|
self.w.arc_ok, self.w.arc_ok_label, self.w.arc_ok_line,
|
|
self.w.move_up, self.w.move_down, self.w.move_label]
|
|
mode1 = [self.w.arc_ok_line,
|
|
self.w.move_up, self.w.move_down, self.w.move_label]
|
|
mode2 = [self.w.offset_label, self.w.arc_voltage_offset,
|
|
self.w.arc_voltage_label, self.w.arc_voltage_line]
|
|
if mode == 1:
|
|
self.w.mode_label.setText('Mode 1')
|
|
for widget in mode1:
|
|
widget.hide()
|
|
elif mode == 2:
|
|
self.w.mode_label.setText('Mode 2')
|
|
for widget in mode2:
|
|
widget.hide()
|
|
else:
|
|
for widget in mode0:
|
|
widget.hide()
|
|
self.w.resize(self.w.minimumSizeHint())
|
|
|
|
def torch_changed(self, halpin):
|
|
if halpin:
|
|
if (hal.get_value('plasmac.mode') == 1 or hal.get_value('plasmac.mode') == 2) and not self.w.arc_ok.isChecked():
|
|
self.w.arc_ok.toggle()
|
|
self.w.arc_ok_clicked()
|
|
else:
|
|
if self.w.arc_ok.isChecked():
|
|
self.w.arc_ok.toggle()
|
|
self.w.arc_ok_clicked()
|
|
|
|
def arc_volts_offset_changed(self, value):
|
|
self.arcVoltsOffsetPin.set(value * 0.1)
|
|
self.w.offset_label.setText(f'{self.arcVoltsOffsetPin.get():0.1f} V')
|
|
|
|
def plasmac_state_changed(self, state):
|
|
if state == 11 and self.torchPin.get():
|
|
self.w.arc_voltage_offset.setEnabled(True)
|
|
else:
|
|
self.w.arc_voltage_offset.setEnabled(False)
|
|
self.w.arc_voltage_offset.setValue(0)
|
|
|
|
def z_position_changed(self, height):
|
|
if self.statePin.get() == 0:
|
|
return
|
|
if self.w.auto_flt.isChecked():
|
|
if self.below_material() and not self.floatPin.get():
|
|
self.float_pressed()
|
|
elif not self.below_material() and self.floatPin.get() and not self.floatLatched:
|
|
self.float_pressed()
|
|
elif self.w.auto_ohm.isChecked():
|
|
if self.below_material() and not self.ohmicPin.get():
|
|
self.ohmic_set()
|
|
elif not self.below_material() and self.ohmicPin.get() and not self.ohmicLatched:
|
|
self.ohmic_reset()
|
|
|
|
def below_material(self):
|
|
mThick = self.materialPin.get() if self.materialPin.get() >= 0 else 0
|
|
mTop = hal.get_value('ini.z.min_limit') + self.height + mThick if mThick else 0
|
|
if self.zPosPin.get() < mTop:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def help_pressed(self):
|
|
msg = QMessageBox(self.w)
|
|
buttonY = msg.addButton(QMessageBox.Ok)
|
|
buttonY.setText('OK')
|
|
buttonY.setFocusPolicy(QtCore.Qt.NoFocus)
|
|
msg.setIcon(QMessageBox.Information)
|
|
msg.setWindowTitle('Sim Panel Help')
|
|
message = 'This panel provides buttons for simulating basic plasma signals.\n'
|
|
message += '\nThe operating mode of QtPlasmaC determines which widgets appear on the panel.\n'
|
|
message += '\nESTOP is required to be cleared before turning the main GUI on and homing the config.\n'
|
|
message += '\nIf a gcode program is loaded and the CYCLE START button pressed then the panel is fully automatic during the running of the program.\n'
|
|
message += '\nThe type of probing is selected by the checkbox below the appropriate button. When the "torch" decends to 10mm (0.39") above the bottom limit then the selected signal will be emitted and the program will continue.\n'
|
|
message += '\nThe ARC OK signal is automatic for all modes.\n'
|
|
message += '\nFor Modes 0 & 1 the ARC VOLTAGE can be adjusted during the "cut" and the "torch" will respond appropriately.\n'
|
|
message += '\nFor Mode 2 the UP and DOWN buttons can be pressed during the "cut" and the "torch" will respond appropriately.\n'
|
|
message += '\nOther buttons can be pressed at any stage of a program run to show the various effect that they have on a running program.\n'
|
|
msg.setText(message)
|
|
msg.exec_()
|
|
|
|
|
|
def get_handlers(halcomp, widgets, paths):
|
|
return [HandlerClass(halcomp, widgets, paths)]
|