''' pmx_test.py Copyright (C) 2020, 2021 Phillip A Carter Copyright (C) 2020, 2021 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 from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * try: import serial import serial.tools.list_ports sMod = True except: sMod = False address = '01' regRead = '04' regWrite = '06' rCurrent = '2094' rCurrentMax = '209A' rCurrentMin = '2099' rFault = '2098' rMode = '2093' rPressure = '2096' rPressureMax = '209D' rPressureMin = '209C' rArcTimeLow = '209E' rArcTimeHigh = '209F' validRead = '0402' class App(QWidget): def __init__(self): super().__init__() if not sMod: msg = '\npyserial module not available\n'\ '\nto install, open a terminal and enter:\n'\ '\nsudo apt-get install python-serial\n' response = QMessageBox() response.setText(msg) response.exec_() raise SystemExit 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.setWindowIcon(QIcon(os.path.join(self.iconBase, self.iconPath))) self.setWindowTitle('Powermax Communicator') qtRectangle = self.frameGeometry() centerPoint = QDesktopWidget().availableGeometry().center() qtRectangle.moveCenter(centerPoint) self.move(qtRectangle.topLeft()) self.createGridLayout() self.setLayout(self.grid) self.show() self.timer = QTimer(self) self.portName.addItem('SELECT A PORT') for item in serial.tools.list_ports.comports(): self.portName.addItem(item.device) self.connected = False self.portName.activated.connect(self.on_port_changed) self.portFile = None self.portScan.pressed.connect(self.on_port_scan) self.usePanel.toggled.connect(self.on_use_toggled) self.modeSet.currentIndexChanged.connect(lambda:self.on_value_changed(self.modeSet, rMode, 1)) self.currentSet.valueChanged.connect(lambda:self.on_value_changed(self.currentSet, rCurrent, 64)) self.pressureSet.valueChanged.connect(lambda:self.on_value_changed(self.pressureSet, rPressure, 128)) self.timer.timeout.connect(self.periodic) self.setStyleSheet( \ 'QWidget {color: #ffee06; background: #16160e} \ QLabel {height: 20} \ QPushButton {border: 1 solid #ffee06; border-radius: 4; height: 30; width: 80} \ QComboBox {color: #ffee06; background-color: #16160e; border: 1 solid #ffee06; border-radius: 4; height: 30; padding-left: 10} \ QComboBox::drop-down {width: 0} \ QComboBox QListView {border: 4p solid #ffee06; border-radius: 0} \ QComboBox QAbstractItemView {border: 2px solid #ffee06; border-radius: 4} \ QDoubleSpinBox {border: 1 solid #ffee06; border-radius: 4; height: 30; width: 80} \ QRadioButton::indicator {border: 1px solid #ffee06; border-radius: 4; height: 20; width: 20} \ QRadioButton::indicator:checked {background: #ffee06} \ QDoubleSpinBox::up-button {subcontrol-origin:padding; subcontrol-position:right; width: 28px; height: 24px} \ QDoubleSpinBox::down-button {subcontrol-origin:padding; subcontrol-position:left; width: 28px; height: 24px} \ ') def periodic(self): if not os.path.exists(self.portFile): self.timer.stop() self.connected = False self.usePanel.setChecked(True) self.useComms.setEnabled(False) self.clear_text() self.portName.clear() self.portName.addItem('SELECT A PORT') try: self.openPort.close() except: pass self.dialog_ok( QMessageBox.Warning,\ 'Error',\ '\nCommunications device lost.\n'\ '\nA Port Scan is required.\n') if self.connected: for reg in (rMode, rCurrent, rPressure, rFault): if not self.read_register(reg): return True def on_value_changed(self, widget, reg, multiplier): if not self.connected: return if reg == rMode: mode = self.modeSet.currentIndex() + 1 self.pressureSet.setValue(0) if not self.write_to_register(rMode, '{:04x}'.format(mode)): return self.mode_changed() return elif reg == rPressure: if self.pressureType == 'bar': if widget.value() == 0.1: widget.setValue(self.minPressure) elif widget.value() == self.minPressure - 0.1: widget.setValue(0) else: if widget.value() == 1: widget.setValue(self.minPressure) elif widget.value() == self.minPressure - 1: widget.setValue(0) data = ('{:04X}'.format(int(widget.value() * multiplier))).upper() elif reg == rCurrent: data = ('{:04X}'.format(int(widget.value() * multiplier))).upper() self.write_to_register(reg , data) def get_lrc(self, data): lrc = 0 for i in range(0, len(data), 2): a, b = data[i:i+2] try: lrc = (lrc + int(a + b, 16)) & 255 except: print('broken packet in get_lrc') return '00' lrc = ('{:02X}'.format((((lrc ^ 255) + 1) & 255))).upper() return lrc def write_to_register(self, reg, data): data = '{}{}{}{}'.format(address, regWrite, reg, data) lrc = self.get_lrc(data) packet = ':{}{}\r\n'.format(data, lrc) errors = 0 while 1: try: reply = '' self.openPort.write(packet.encode()) reply = self.openPort.readline().decode() except: return False if reply == packet: break else: errors += 1 if errors == 3: self.connected = False self.usePanel.setChecked(True) self.dialog_ok( QMessageBox.Warning,\ 'Error',\ '\nNo reply while writing to plasma unit.\n'\ '\nCheck connections and retry when ready.\n') return False return True def read_from_register(self, reg): data = '{}{}{}0001'.format(address, regRead, reg) lrc =self.get_lrc(data) packet = ':{}{}\r\n'.format(data, lrc) reply = '' self.openPort.write(packet.encode()) reply = self.openPort.readline().decode() if reply: return reply else: self.connected = False self.usePanel.setChecked(True) self.dialog_ok(QMessageBox.Warning,\ 'Error',\ '\nNo reply while reading from plasma unit.\n'\ '\nCheck connections and retry when ready.\n') return None def read_register(self, reg): try: result = self.read_from_register(reg).strip().lstrip(':') except: return if result: if int(result.strip(), 16) >= 0: if result[:6] == '{}{}'.format(address, validRead): lrc = self.get_lrc('{}'.format(result[:10])) if lrc == result[10:12]: if reg == rMode: data = int(result[6:10]) self.modeValue.setText(str(data)) return data elif reg == rCurrent: data = float(int(result[6:10], 16) / 64.0) self.currentValue.setText('{:.0f}'.format(data)) return data elif reg == rPressure: data = float(int(result[6:10], 16) / 128.0) if self.pressureType == 'bar': self.pressureValue.setText('{:.1f}'.format(data)) else: self.pressureValue.setText('{:.0f}'.format(data)) return 1 elif reg == rFault: fault = int(result[6:10], 16) code = '{:04d}'.format(fault) if fault > 0: self.faultLabel.setText('FAULT') self.faultValue.setText('{}-{}-{}'.format(code[0], code[1:3], code[3])) else: self.faultLabel.setText('') self.faultValue.setText('') if fault == 210: if float(self.currentMax.text()) >110: self.faultName.setText('{}'.format(faultCode[code][1])) else: self.faultName.setText('{}'.format(faultCode[code][1])) else: try: self.faultName.setText('{}'.format(faultCode[code])) except: self.faultName.setText('UNKNOWN FAULT CODE') return code elif reg == rCurrentMin: data = float(int(result[6:10], 16) / 64.0) self.currentMin.setText('{:.0f}'.format(data)) return data elif reg == rCurrentMax: data = float(int(result[6:10], 16) / 64.0) self.currentMax.setText('{:.0f}'.format(data)) return data elif reg == rPressureMin: data = float(int(result[6:10], 16) / 128.0) self.minimumPressure = data if data < 15: self.pressureType = 'bar' self.pressureMin.setText('{:.1f}'.format(data)) self.pressure.setSingleStep(0.1) self.pressure.setDecimals(1) else: self.pressureType = 'psi' self.pressureMin.setText('{:.0f}'.format(data)) self.pressureSet.setSingleStep(1) self.pressureSet.setDecimals(0) return data elif reg == rPressureMax: data = float(int(result[6:10], 16) / 128.0) if self.pressureType == 'bar': self.pressureMax.setText('{:.1f}'.format(data)) self.pressureSet.setMaximum(data) else: self.pressureMax.setText('{:.0f}'.format(data)) self.pressureSet.setMaximum(data) return data elif reg == rArcTimeLow: data = result[6:10] return data elif reg == rArcTimeHigh: data = result[6:10] return data def on_use_toggled(self): if self.usePanel.isChecked(): if self.connected: self.connected = False if not self.write_to_register(rMode, '0000'): return if not self.write_to_register(rCurrent, '0000'): return if not self.write_to_register(rPressure, '0000'): return self.clear_text() self.portName.setEnabled = True else: if self.currentSet.value() == 0: result = self.dialog_ok( QMessageBox.Warning,\ 'Error',\ '\nA value is required for Current.\n') if result: self.usePanel.setEnabled(True) return self.portName.setEnabled = False mode = self.modeSet.currentIndex() + 1 if not self.write_to_register(rMode, '{:04x}'.format(mode)): return data = '{:04X}'.format(int(self.currentSet.value() * 64)) if not self.write_to_register(rCurrent, data): return data = '{:04X}'.format(int(self.pressureSet.value() * 128)) if not self.write_to_register(rPressure, data): return self.mode_changed() self.timer.start(100) self.connected = True def mode_changed(self): if not self.read_register(rCurrentMin): return if not self.read_register(rCurrentMax): return self.currentSet.setRange(int(float(self.currentMin.text())),int(float(self.currentMax.text()))) if not self.read_register(rPressureMin): return if not self.read_register(rPressureMax): return if not self.read_register(rFault): return ArcTimeLow = self.read_register(rArcTimeLow) ArcTimeHigh = self.read_register(rArcTimeHigh) if ArcTimeLow and ArcTimeHigh: ArcTime = int((ArcTimeHigh + ArcTimeLow), 16) m, s = divmod(ArcTime, 60) h, m = divmod(m, 60) self.arctimeValue.setText('{:.0f}:{:02.0f}:{:02.0f}'.format(h,m,s)) self.pressureSet.setRange((0),float(self.pressureMax.text())) self.minPressure = float(self.pressureMin.text()) self.maxPressure = float(self.pressureMax.text()) def on_port_scan(self): self.usePanel.setChecked(True) self.timer.stop() self.clear_text() try: self.openPort.close() except: pass self.portName.clear() self.portName.addItem('SELECT A PORT') for item in serial.tools.list_ports.comports(): self.portName.addItem(item.device) self.portName.showPopup() self.usePanel.setEnabled(False) self.useComms.setEnabled(False) self.portName.setCurrentIndex( self.portName.count() - 1 ) self.on_port_changed() def on_port_changed(self): self.usePanel.setChecked(True) self.usePanel.setEnabled(False) self.useComms.setEnabled(False) if self.portName.currentText() == 'SELECT A PORT': return try: self.openPort.close() except: pass try: self.openPort = serial.Serial( self.portName.currentText(), baudrate = 19200, bytesize = 8, parity = 'E', stopbits = 1, timeout = 0.1 ) print('\n{} is open...\n'.format(self.portName.currentText())) except: self.dialog_ok( QMessageBox.Warning,\ 'Error',\ '\nCould not open {}\n'.format(self.portName.currentText())) return self.usePanel.setEnabled(True) self.useComms.setEnabled(True) self.portFile = self.portName.currentText() def clear_text(self): self.modeValue.setText('') self.currentValue.setText('') self.pressureValue.setText('') self.faultValue.setText('') self.currentMin.setText('') self.pressureMin.setText('') self.faultLabel.setText('') self.faultName.setText('') self.currentMax.setText('') self.pressureMax.setText('') self.arctimeValue.setText('') self.modeSet.setCurrentIndex(0) self.currentSet.setValue(40) self.pressureSet.setValue(0) def dialog_ok(self,icon,title,text): response = QMessageBox() response.setIcon(icon) response.setWindowIcon(QIcon(os.path.join(self.iconBase, self.iconPath))) response.setWindowTitle(title) response.setText(text); response.exec_() return response def createGridLayout(self): self.grid = QGridLayout() for r in range(0, 8): self.grid.setRowMinimumHeight(r, 32) for c in range(0, 5): self.grid.setColumnMinimumWidth(c, 100) self.portScan = QPushButton('PORT SCAN') self.grid.addWidget(self.portScan,0,0) self.portName = QComboBox() self.portName.setStyleSheet('QComboBox {width: 200}') self.grid.addWidget(self.portName,0,2,1,2) self.usePanel = QRadioButton('PANEL') self.usePanel.setChecked(True) self.usePanel.setEnabled(False) self.grid.addWidget(self.usePanel,0,4) self.useComms = QRadioButton('RS485') self.useComms.setEnabled(False) self.grid.addWidget(self.useComms,1,4) self.minLabel = QLabel('MIN.') self.minLabel.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.minLabel,2,1) self.maxLabel = QLabel('MAX.') self.maxLabel.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.maxLabel,2,2) self.valueLabel = QLabel('VALUE') self.valueLabel.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.valueLabel,2,3) self.setLabel = QLabel('SET TO') self.setLabel.setAlignment(Qt.AlignCenter| Qt.AlignVCenter) self.grid.addWidget(self.setLabel,2,4) self.modeLabel = QLabel('MODE') self.modeLabel.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.modeLabel,3,0) self.currentLabel = QLabel('CURRENT') self.currentLabel.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.currentLabel,4,0) self.pressureLabel = QLabel('PRESSURE') self.pressureLabel.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.pressureLabel,5,0) self.arctimeLabel = QLabel('ARC ON TIME') self.arctimeLabel.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.arctimeLabel,6,0) self.faultLabel = QLabel('ERROR') self.faultLabel.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.faultLabel,7,0) self.modeValue = QLabel('0') self.modeValue.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.modeValue,3,3) self.currentValue = QLabel('0') self.currentValue.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.currentValue,4,3) self.pressureValue = QLabel('0') self.pressureValue.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.pressureValue,5,3) self.arctimeValue = QLabel('0') self.arctimeValue.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.arctimeValue,6,3) self.faultValue = QLabel('0') self.faultValue.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.faultValue,7,1) self.currentMin = QLabel('0') self.currentMin.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.currentMin,4,1) self.pressureMin = QLabel('0') self.pressureMin.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.pressureMin,5,1) self.currentMax = QLabel('0') self.currentMax.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.currentMax,4,2) self.pressureMax = QLabel('0') self.pressureMax.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.pressureMax,5,2) self.faultName = QLabel('') self.faultName.setAlignment(Qt.AlignLeft| Qt.AlignVCenter) self.grid.addWidget(self.faultName,7,2,1,3) self.modeSet = QComboBox() self.modeSet.addItems(['NORMAL','CPA','GOUGE']) self.modeSet.setCurrentIndex(0) self.grid.addWidget(self.modeSet,3,4) self.currentSet = QDoubleSpinBox() self.currentSet.setMaximum(125) self.currentSet.setWrapping(True) self.currentSet.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.currentSet,4,4) self.pressureSet = QDoubleSpinBox() self.pressureSet.setMaximum(125) self.pressureSet.setWrapping(True) self.pressureSet.setAlignment(Qt.AlignRight| Qt.AlignVCenter) self.grid.addWidget(self.pressureSet,5,4) self.clear_text() def shut_down(self): if self.connected: self.write_to_register(rMode, '0000') self.write_to_register(rCurrent, '0000') self.write_to_register(rPressure, '0000') faultCode = { '0000': '', '0110': 'Remote controller mode invalid', '0111': 'Remote controller current invalid', '0112': 'Remote controller pressure invalid', '0120': 'Low input gas pressure', '0121': 'Output gas pressure low', '0122': 'Output gas pressure high', '0123': 'Output gas pressure unstable', '0130': 'AC input power unstable', '0199': 'Power board hardware protection', '0200': 'Low gas pressure', '0210': ('Gas flow lost while cutting', 'Excessive arc voltage'), '0220': 'No gas input', '0300': 'Torch stuck open', '0301': 'Torch stuck closed', '0320': 'End of consumable life', '0400': 'PFC/Boost IGBT module under temperature', '0401': 'PFC/Boost IGBT module over temperature', '0402': 'Inverter IGBT module under temperature', '0403': 'Inverter IGBT module over temperature', '0500': 'Retaining cap off', '0510': 'Start/trigger signal on at power up', '0520': 'Torch not connected', '0600': 'AC input voltage phase loss', '0601': 'AC input voltage too low', '0602': 'AC input voltage too high', '0610': 'AC input unstable', '0980': 'Internal communication failure', '0990': 'System hardware fault', '1000': 'Digital signal processor fault', '1100': 'A/D converter fault', '1200': 'I/O fault', '2000': 'A/D converter value out of range', '2010': 'Auxiliary switch disconnected', '2100': 'Inverter module temp sensor open', '2101': 'Inverter module temp sensor shorted', '2110': 'Pressure sensor is open', '2111': 'Pressure sensor is shorted', '2200': 'DSP does not recognize the torch', '3000': 'Bus voltage fault', '3100': 'Fan speed fault', '3101': 'Fan fault', '3110': 'PFC module temperature sensor open', '3111': 'PFC module temperature sensor shorted', '3112': 'PFC module temperature sensor circuit fault', '3200': 'Fill valve', '3201': 'Dump valve', '3201': 'Valve ID', '3203': 'Electronic regulator is disconnected', '3410': 'Drive fault', '3420': '5 or 24 VDC fault', '3421': '18 VDC fault', '3430': 'Inverter capacitors unbalanced', '3441': 'PFC over current', '3511': 'Inverter saturation fault', '3520': 'Inverter shoot-through fault', '3600': 'Power board fault', '3700': 'Internal serial communications fault', } if __name__ == '__main__': app = QApplication(sys.argv) ex = App() app.aboutToQuit.connect(ex.shut_down) sys.exit(app.exec_())