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