From 53774cae466484e5355652350090a46847558267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Arensk=C3=B6tter?= Date: Sat, 4 Nov 2023 13:51:51 +0100 Subject: [PATCH] First version for server&client --- src/MCP4822.py | 5 +- src/PID.py | 219 +++++++++++++++++ src/clientthread.py | 210 ++++++++++++++++ src/globalvars.py | 4 + src/gui-client/channel.py | 116 +++++++++ src/gui-client/channel.ui | 208 ++++++++++++++++ src/gui-client/channelInfoClass.py | 170 +++++++++++++ src/gui-client/channelInfoWidgetClass.py | 24 ++ src/gui-client/client.py | 170 +++++++++++++ src/gui-client/globalvars.py | 6 + src/gui-client/main_window.py | 187 ++++++++++++++ src/gui-client/main_window.ui | 300 +++++++++++++++++++++++ src/gui-client/pin.py | 129 ++++++++++ src/gui-client/runGui.bat | 11 + src/gui-client/settings.json | 1 + src/gui-client/tcptools.py | 31 +++ src/server.py | 147 +++++++++++ src/spithread.py | 112 +++++++++ src/tcptools.py | 31 +++ 19 files changed, 2079 insertions(+), 2 deletions(-) create mode 100644 src/PID.py create mode 100644 src/clientthread.py create mode 100644 src/globalvars.py create mode 100644 src/gui-client/channel.py create mode 100644 src/gui-client/channel.ui create mode 100644 src/gui-client/channelInfoClass.py create mode 100644 src/gui-client/channelInfoWidgetClass.py create mode 100644 src/gui-client/client.py create mode 100644 src/gui-client/globalvars.py create mode 100644 src/gui-client/main_window.py create mode 100644 src/gui-client/main_window.ui create mode 100644 src/gui-client/pin.py create mode 100644 src/gui-client/runGui.bat create mode 100644 src/gui-client/settings.json create mode 100644 src/gui-client/tcptools.py create mode 100644 src/server.py create mode 100644 src/spithread.py create mode 100644 src/tcptools.py diff --git a/src/MCP4822.py b/src/MCP4822.py index 807f811..6480eef 100644 --- a/src/MCP4822.py +++ b/src/MCP4822.py @@ -20,7 +20,7 @@ class MCP4822: ''' Constructor of the class, setup the GPIO pin for SPI-communication ''' - def __init__(self, CLK_Pin, MOSI_Pin, CS_Pin, LDAC_Pin, vref=None): + def __init__(self, CLK_Pin, MOSI_Pin, CS_Pin, LDAC_Pin, nullVoltage=0, vref=None): GPIO.setmode(GPIO.BCM) self.clkPin = CLK_Pin @@ -43,7 +43,8 @@ class MCP4822: GPIO.output(self.mosiPin, GPIO.LOW) GPIO.output(self.ldacPin, GPIO.LOW) - self.write(1.29,1) + self.nullVoltage = nullVoltage + self.write(self.nullVoltage,1) def U2dac(self,U,DAC): #U: voltage (V) [0V-4.096V] diff --git a/src/PID.py b/src/PID.py new file mode 100644 index 0000000..60e0828 --- /dev/null +++ b/src/PID.py @@ -0,0 +1,219 @@ +''' +Created on Tue Jan 9 2018 + +@author: jan +''' + +import time +import globalvars +import queue +import zmq + +class PID: + def __init__(self, dev_num_adc, channel_num_adc, dev_num_dac, channel_num_dac, pid_num, kp=None, ki=None, kd=None, setpoint=None): + if kp is None: + self.kp = 1 + else: + self.kp = float(kp) + + if ki is None: + self.ki = 1 + else: + self.ki = float(ki) + + if kd is None: + self.kd = 1 + else: + self.kd = float(kd) + + if setpoint is None: + self.setpoint = 0.0 + else: + self.setpoint = float(setpoint) + + self.dev_num_adc = dev_num_adc + self.channel_num_adc = channel_num_adc + self.dev_num_dac = dev_num_dac + self.channel_dac = channel_num_dac + self.pid_num = pid_num + + self.ITerm = 0 + self.lastInput = self.setpoint + self.outMin = 0 + self.outMax = 5 + self.error = 0 + + self.start = time.time() + self.stop = 0 + + # Order of initialisation shouldn't matter, because zmq handel everything + self.context = zmq.Context(1) + self.SPIclient = self.context.socket(zmq.REQ) + self.SPIclient.connect("tcp://localhost:5555") + + def computePID(self): + #Get ADC value + #self.out_queue.put(['ADC', self.dev_num, self.channel_num, self.pid_num]) + #Input = self.in_queue.get() + msg = ['ADC', self.dev_num_adc, self.channel_num_adc, self.pid_num] + self.SPIclient.send('\t'.join(str(x) for x in msg).encode()) + Input = float(self.SPIclient.recv().decode()) + + #Get elapsed time for right PID constants + self.stop = time.time() + elapsed_time = self.stop - self.start + self.start = time.time() + + #print(type(self.ki)) + Ki = float(self.ki) * elapsed_time + Kd = float(self.kd) / elapsed_time + + #Calculate working variables + self.error = float(self.setpoint) - Input + dInput = Input - self.lastInput + self.ITerm = self.ITerm + (Ki * self.error) + # Check min-max for I-Term + if self.ITerm > self.outMax: + self.ITerm = self.outMax + elif self.ITerm < self.outMin: + self.ITerm = self.outMin + + # Calculate PID Output + self.Output = float(self.kp) * self.error + self.ITerm - Kd * dInput + # Check min-max for output + if self.Output > self.outMax: + self.Output = self.outMax + elif self.Output < self.outMin: + self.Output = self.outMin + + # Save variable for next run + self.lastInput = Input + + # Set output value + #self.out_queue.put(['DAC', self.dev_num, self.channel_num, self.pid_num, self.Output]) + msg = ['DAC', self.dev_num_dac, self.channel_num_dac, self.pid_num, self.Output] + self.SPIclient.send('\t'.join(str(x) for x in msg).encode()) + ans = self.SPIclient.recv().decode() + #if self.in_queue.get() != 'ACK': + if ans != 'ACK': + print('Error setting DAC-value') + + def setSetpoint(self, setpoint): + #self.ITerm = 0 + self.setpoint = setpoint + + def setKp(self,P): + self.kp=P + + def setKi(self,I): + self.ki=I + + def setKd(self,D): + self.kd=D + + def setOutmax(self,outMax): + self.outMax = outMax + + def getSetpoint(self): + return self.setpoint + + def getError(self): + return self.error + + def getKp(self): + return self.kp + + def getKi(self): + return self.ki + + def getKd(self): + return self.kd + + def getOutput(self): + return self.Output + + def getTemperatur(self): + return self.lastInput + + def getRunValues(self): + return [self.lastInput, self.error, self.Output] + + +def run(pid_obj, freq, num): + context = zmq.Context() + server = context.socket(zmq.REP) + port = 5556 + num + server.bind("tcp://*:%d" % port) + poller = zmq.Poller() + poller.register(server, zmq.POLLIN) + + stop = False + + while not globalvars.stopall and not stop: + cmd_recv = '' + if globalvars.pidrun[num]: + pid_obj.computePID() + #try: + # msg = in_queue.get(True,1/freq) + #except queue.Empty: + # msg = ['None'] + socks = dict(poller.poll(1000)) + if server in socks and socks[server] == zmq.POLLIN: + cmd_recv = server.recv_string() + else: + cmd_recv = 'None' + else: + socks = dict(poller.poll(1000)) + if server in socks and socks[server] == zmq.POLLIN: + cmd_recv = server.recv_string() + + msg = cmd_recv.split('\t') + ans = 'ACK' + if msg[0] == 'None': + pass + elif msg[0] == 'set_setpoint': + pid_obj.setSetpoint(msg[1]) + server.send_string('ACK') + elif msg[0] == 'set_kp': + pid_obj.setKp(msg[1]) + server.send_string('ACK') + elif msg[0] == 'set_ki': + pid_obj.setKi(msg[1]) + server.send_string('ACK') + elif msg[0] == 'set_kd': + pid_obj.setKd(msg[1]) + server.send_string('ACK') + elif msg[0] == 'set_outmax': + pid_obj.setOutmax(msg[1]) + server.send_string('ACK') + elif msg[0] == 'get_setpoint': + #out_queue.put(pid_obj.getSetpoint() ) + server.send_string(str(pid_obj.getSetpoint())) + elif msg[0] == 'get_error': + #out_queue.put(pid_obj.getError() ) + server.send_string(str(pid_obj.getError())) + elif msg[0] == 'get_kp': + #out_queue.put(pid_obj.getKp() ) + server.send_string(str(pid_obj.getKp())) + elif msg[0] == 'get_ki': + #out_queue.put(pid_obj.getKi() ) + server.send_string(str(pid_obj.getKi())) + elif msg[0] == 'get_kd': + #out_queue.put(pid_obj.getKd() ) + server.send_string(str(pid_obj.getKd())) + elif msg[0] == 'lock': + print('Starting PID') + server.send_string('Starting PID') + elif msg[0] == 'get_output': + #out_queue.put(pid_obj.getOutput() ) + server.send_string(str(pid_obj.getOutput())) + elif msg[0] == 'get_temperatur': + #out_queue.put(pid_obj.getTemperatur() ) + server.send_string(str(pid_obj.getTemperatur())) + elif msg[0] == 'get_run_values': + #out_queue.put(pid_obj.getRunValues() ) + server.send_string(str(pid_obj.getRunValues())) + + elif msg[0] == 'END': + server.send_string('ACK') + stop = True diff --git a/src/clientthread.py b/src/clientthread.py new file mode 100644 index 0000000..7202b57 --- /dev/null +++ b/src/clientthread.py @@ -0,0 +1,210 @@ +import queue +import socket + +import zmq +import globalvars +import tcptools as TCP +#TCP/IP client thread +def clientthread(conn): + global lock,busy + welcom_msg = '--> Welcome to the Time-Bin-temperature controller <--\n' + welcom_msg = welcom_msg + ' '*(1024-len(welcom_msg)) + conn.send(welcom_msg.encode()) + print('\nopen thread...') + + #Connect to SPI thread via zmq tcp socket + context = zmq.Context(1) + SPIclient = context.socket(zmq.REQ) + SPIclient.connect("tcp://localhost:5555") + + #Connect to each pid threads + pidclient = [0] + context = zmq.Context(1) + pidclient[0] = context.socket(zmq.REQ) + pidclient[0].connect("tcp://localhost:5556") + + + while not globalvars.stopall: + try: + cmd_recv = conn.recv(1024) + except: + print('\nSocket broken') + break + if not cmd_recv: + break + if globalvars.stopall: + break + + ans = '' + + #split command + tmp = cmd_recv.decode().split('\t') + + try: + tmp[2] = int(str2num(tmp[2])) + tmp[3] = str2num(tmp[3]) + except IndexError: + pass + + #print(tmp) + + if tmp[0] == 'CMD:': + if tmp[1] == 'close': + TCP.send_msg(conn,'ANS: Bye') + break + elif tmp[1] == 'IDLE': + globalvars.pidrun[tmp[2]] = False + TCP.send_msg(conn, 'I set PID {:.0f} to {:.0f}'.format(tmp[2],False)) + elif tmp[1] == 'LOCK': + globalvars.pidrun[tmp[2]] = True + #out_queue[tmp[2]].put(['lock']) + pidclient[tmp[2]].send_string('lock') + pidclient[tmp[2]].recv_string() + TCP.send_msg(conn, 'I set PID {:.0f} to {:.0f}'.format(tmp[2],True)) + elif tmp[1] == 'SET_SETPOINT': + #out_queue[tmp[2]].put(['set_setpoint',tmp[3]]) + pidclient[tmp[2]].send_string('set_setpoint\t%f' % tmp[3]) + pidclient[tmp[2]].recv_string() + TCP.send_msg(conn, 'I set the setpoint for output {:.0f} to {: f}'.format(tmp[2],tmp[3])) + elif tmp[1] == 'SET_KP': + #out_queue[tmp[2]].put(['set_kp',tmp[3]]) + pidclient[tmp[2]].send_string('set_kp\t%f' % tmp[3]) + pidclient[tmp[2]].recv_string() + TCP.send_msg(conn, 'I set kp for output {:.0f} to {: f}'.format(tmp[2],tmp[3])) + elif tmp[1] == 'SET_KI': + #out_queue[tmp[2]].put(['set_ki',tmp[3]]) + pidclient[tmp[2]].send_string('set_ki\t%f' % tmp[3]) + pidclient[tmp[2]].recv_string() + TCP.send_msg(conn, 'I set ki for output {:.0f} to {: f}'.format(tmp[2],tmp[3])) + elif tmp[1] == 'SET_KD': + #out_queue[tmp[2]].put(['set_kd',tmp[3]]) + pidclient[tmp[2]].send_string('set_kd\t%f' % tmp[3]) + pidclient[tmp[2]].recv_string() + TCP.send_msg(conn, 'I set kd for output {:.0f} to {: f}'.format(tmp[2],tmp[3])) + elif tmp[1] == 'SET_OUTMAX': + #out_queue[tmp[2]].put(['set_outmax',tmp[3]]) + pidclient[tmp[2]].send_string('set_outmax\t%f' % tmp[3]) + pidclient[tmp[2]].recv_string() + TCP.send_msg(conn, 'I set maximal output voltage for output {:.0f} to {: f}'.format(tmp[2],tmp[3])) + elif tmp[1] == 'GET_SETPOINT': + #out_queue[tmp[2]].put(['get_setpoint']) + pidclient[tmp[2]].send_string('get_setpoint') + ans = float(pidclient[tmp[2]].recv_string()) + #ans = in_queue[tmp[2]].get() + TCP.send_msg(conn, ans) + elif tmp[1] == 'GET_ERROR': + #out_queue[tmp[2]].put(['get_error']) + pidclient[tmp[2]].send_string('get_error') + ans = float(pidclient[tmp[2]].recv_string()) + #ans = in_queue[tmp[2]].get() + TCP.send_msg(conn, ans) + elif tmp[1] == 'GET_KP': + #out_queue[tmp[2]].put(['get_kp']) + pidclient[tmp[2]].send_string('get_kp') + ans = float(pidclient[tmp[2]].recv_string()) + #ans = in_queue[tmp[2]].get() + TCP.send_msg(conn, ans) + elif tmp[1] == 'GET_KI': + #out_queue[tmp[2]].put(['get_ki']) + pidclient[tmp[2]].send_string('get_ki') + ans = float(pidclient[tmp[2]].recv_string()) + #ans = in_queue[tmp[2]].get() + TCP.send_msg(conn, ans) + elif tmp[1] == 'GET_KD': + #out_queue[tmp[2]].put(['get_kd']) + pidclient[tmp[2]].send_string('get_kd') + ans = float(pidclient[tmp[2]].recv_string()) + #ans = in_queue[tmp[2]].get() + TCP.send_msg(conn, ans) + elif tmp[1] == 'GET_PID_STATUS': + ans = int(globalvars.pidrun[tmp[2]]) + TCP.send_msg(conn, ans) + elif tmp[1] == 'GET_TEMP': + if globalvars.pidrun[tmp[2]]: + #out_queue[tmp[2]].put(['get_temperatur']) + pidclient[tmp[2]].send_string('get_temperatur') + ans = float(pidclient[tmp[2]].recv_string()) + #ans = in_queue[tmp[2]].get() + TCP.send_msg(conn, ans) + else: + if tmp[2] == 0: + dev_chan = ['ADC', 0, 0, 4] + + + #SPIQueue.put(dev_chan) + SPIclient.send('\t'.join(str(x) for x in dev_chan).encode()) + ans = SPIclient.recv().decode() + #ans = tcpqueue.get() + TCP.send_msg(conn, ans) + + elif tmp[1] == 'GET_RUN_VALUES': + if globalvars.pidrun[tmp[2]]: + #out_queue[tmp[2]].put(['get_run_values']) + pidclient[tmp[2]].send_string('get_run_values') + ans = eval(pidclient[tmp[2]].recv_string()) + #ans = in_queue[tmp[2]].get() + TCP.send_msg(conn, ans) + else: + if tmp[2] == 0: + dev_chan = ['ADC', 0, 0, 4] + + + #SPIQueue.put(dev_chan) + #ans = tcpqueue.get() + SPIclient.send('\t'.join(str(x) for x in dev_chan).encode()) + ans = float(SPIclient.recv().decode()) + #adc value, 0, output voltage + TCP.send_msg(conn, [ans, 0, globalvars.output_volt[tmp[2]]]) + + elif tmp[1] == 'GET_VOLTAGE': + if globalvars.pidrun[tmp[2]]: + #out_queue[tmp[2]].put(['get_output']) + pidclient[tmp[2]].send_string('get_output') + ans = float(pidclient[tmp[2]].recv_string()) + #ans = in_queue[tmp[2]].get() + TCP.send_msg(conn, ans) + globalvars.output_volt[tmp[2]] = ans + else: + TCP.send_msg(conn, globalvars.output_volt[tmp[2]]) + + elif tmp[1] == 'SET_VOLTAGE': + if tmp[2] == 0: + dev_chan = ['DAC', 0, 0, 4, tmp[3]] + elif tmp[2] == 1: + dev_chan = ['DAC', 0, 1, 4, tmp[3]] + + + globalvars.output_volt[tmp[2]] = tmp[3] + + #SPIQueue.put(dev_chan) + #ans = tcpqueue.get() + SPIclient.send('\t'.join(str(x) for x in dev_chan).encode()) + ans = SPIclient.recv().decode() + if ans == 'ACK': + TCP.send_msg(conn, 'I set voltage of output {:.0f} to {: f}'.format(tmp[2],tmp[3])) + else: + print('Error!') + else: + print('Not a command!') + #reply = reply + ' '*(1024 - len(reply)) + #conn.send(reply.encode()) + busy = False + + busy = False + if not globalvars.stopall: + print('close thread') + else: + print('close thread because of global stop') + #reply = reply + ' '*(1024 - len(reply)) + #conn.sendall(reply.encode()) + conn.close() + +#Function to convert byte string to int/float +def str2num(s): + try: + return int(s) + except ValueError: + try: + return float(s) + except ValueError: + return 0 diff --git a/src/globalvars.py b/src/globalvars.py new file mode 100644 index 0000000..5966e17 --- /dev/null +++ b/src/globalvars.py @@ -0,0 +1,4 @@ + +stopall = False +pidrun = [False, False, False, False] +output_volt = [0,0,0,0] diff --git a/src/gui-client/channel.py b/src/gui-client/channel.py new file mode 100644 index 0000000..a57a426 --- /dev/null +++ b/src/gui-client/channel.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'channel.ui' +# +# Created by: PyQt5 UI code generator 5.14.1 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(846, 423) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setObjectName("gridLayout") + spacerItem = QtWidgets.QSpacerItem(20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem, 6, 1, 1, 1) + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.label_2 = QtWidgets.QLabel(Form) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + self.gridLayout.addItem(spacerItem1, 7, 1, 1, 1) + self.PIDenableButton = QtWidgets.QPushButton(Form) + self.PIDenableButton.setStyleSheet("background-color: rgb(255,0,0)") + self.PIDenableButton.setCheckable(True) + self.PIDenableButton.setObjectName("PIDenableButton") + self.gridLayout.addWidget(self.PIDenableButton, 4, 0, 1, 1) + self.setpointBox = QtWidgets.QDoubleSpinBox(Form) + self.setpointBox.setDecimals(4) + self.setpointBox.setSingleStep(0.01) + self.setpointBox.setProperty("value", 22.0) + self.setpointBox.setObjectName("setpointBox") + self.gridLayout.addWidget(self.setpointBox, 0, 1, 1, 1) + self.kdBox = QtWidgets.QDoubleSpinBox(Form) + self.kdBox.setMinimum(-99.0) + self.kdBox.setSingleStep(0.01) + self.kdBox.setProperty("value", 1.0) + self.kdBox.setObjectName("kdBox") + self.gridLayout.addWidget(self.kdBox, 3, 1, 1, 1) + self.outputBox = QtWidgets.QDoubleSpinBox(Form) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.outputBox.sizePolicy().hasHeightForWidth()) + self.outputBox.setSizePolicy(sizePolicy) + self.outputBox.setMaximumSize(QtCore.QSize(100, 16777215)) + self.outputBox.setDecimals(3) + self.outputBox.setSingleStep(0.01) + self.outputBox.setObjectName("outputBox") + self.gridLayout.addWidget(self.outputBox, 5, 0, 1, 1) + self.label_4 = QtWidgets.QLabel(Form) + self.label_4.setObjectName("label_4") + self.gridLayout.addWidget(self.label_4, 0, 0, 1, 1) + self.kpBox = QtWidgets.QDoubleSpinBox(Form) + self.kpBox.setMinimum(-99.0) + self.kpBox.setSingleStep(0.01) + self.kpBox.setProperty("value", 1.0) + self.kpBox.setObjectName("kpBox") + self.gridLayout.addWidget(self.kpBox, 1, 1, 1, 1) + self.label_3 = QtWidgets.QLabel(Form) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1) + self.OutputButton = QtWidgets.QPushButton(Form) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.OutputButton.sizePolicy().hasHeightForWidth()) + self.OutputButton.setSizePolicy(sizePolicy) + self.OutputButton.setMaximumSize(QtCore.QSize(100, 16777215)) + self.OutputButton.setObjectName("OutputButton") + self.gridLayout.addWidget(self.OutputButton, 5, 1, 1, 1) + self.kiBox = QtWidgets.QDoubleSpinBox(Form) + self.kiBox.setMinimum(-99.0) + self.kiBox.setSingleStep(0.01) + self.kiBox.setProperty("value", 1.0) + self.kiBox.setObjectName("kiBox") + self.gridLayout.addWidget(self.kiBox, 2, 1, 1, 1) + self.widget = QtWidgets.QWidget(Form) + self.widget.setObjectName("widget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.widget) + self.gridLayout_2.setObjectName("gridLayout_2") + self.widget_2 = QtWidgets.QWidget(self.widget) + self.widget_2.setObjectName("widget_2") + self.temp_layout = QtWidgets.QHBoxLayout(self.widget_2) + self.temp_layout.setObjectName("temp_layout") + self.gridLayout_2.addWidget(self.widget_2, 0, 0, 1, 1) + self.widget_3 = QtWidgets.QWidget(self.widget) + self.widget_3.setObjectName("widget_3") + self.error_layout = QtWidgets.QHBoxLayout(self.widget_3) + self.error_layout.setObjectName("error_layout") + self.gridLayout_2.addWidget(self.widget_3, 0, 1, 1, 1) + self.widget_4 = QtWidgets.QWidget(self.widget) + self.widget_4.setObjectName("widget_4") + self.output_layout = QtWidgets.QHBoxLayout(self.widget_4) + self.output_layout.setObjectName("output_layout") + self.gridLayout_2.addWidget(self.widget_4, 1, 0, 1, 2) + self.gridLayout.addWidget(self.widget, 0, 2, 8, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.label.setText(_translate("Form", "kp")) + self.label_2.setText(_translate("Form", "ki")) + self.PIDenableButton.setText(_translate("Form", "PID enable")) + self.label_4.setText(_translate("Form", "Setpoint (°C)")) + self.label_3.setText(_translate("Form", "kd")) + self.OutputButton.setText(_translate("Form", "Set output")) diff --git a/src/gui-client/channel.ui b/src/gui-client/channel.ui new file mode 100644 index 0000000..46cfeea --- /dev/null +++ b/src/gui-client/channel.ui @@ -0,0 +1,208 @@ + + + Form + + + + 0 + 0 + 846 + 423 + + + + Form + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 10 + + + + + + + + kp + + + + + + + ki + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 20 + + + + + + + + background-color: rgb(255,0,0) + + + PID enable + + + true + + + + + + + 4 + + + 0.010000000000000 + + + 22.000000000000000 + + + + + + + -99.000000000000000 + + + 0.010000000000000 + + + 1.000000000000000 + + + + + + + + 0 + 0 + + + + + 100 + 16777215 + + + + 3 + + + 0.010000000000000 + + + + + + + Setpoint (°C) + + + + + + + -99.000000000000000 + + + 0.010000000000000 + + + 1.000000000000000 + + + + + + + kd + + + + + + + + 0 + 0 + + + + + 100 + 16777215 + + + + Set output + + + + + + + -99.000000000000000 + + + 0.010000000000000 + + + 1.000000000000000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/gui-client/channelInfoClass.py b/src/gui-client/channelInfoClass.py new file mode 100644 index 0000000..f0cc6af --- /dev/null +++ b/src/gui-client/channelInfoClass.py @@ -0,0 +1,170 @@ +from PyQt5.QtCore import QObject + +import numpy as np +import ast +import socket + +import tcptools as TCP +import globalvars + +from channelInfoWidgetClass import channelInfoWidget + +class channelInfo(QObject): + def __init__(self, channel_num, temp_field, out_field, enable_field, tabnumber, activTabWidget, parent): + super(channelInfo,self).__init__(parent) + self.widget_obj = channelInfoWidget(self) + self.channel_num = channel_num + + self.temp_field = temp_field + self.out_field = out_field + self.enable_field = enable_field + + self.tabNumber = tabnumber + self.activTabWidget = activTabWidget + + self.data_length = 64 + self.temp_data = np.array([21.0] * self.data_length) + self.error_data = np.array([0.0] * self.data_length) + self.output_data = np.array([0.0] * self.data_length) + self.interror_data = np.array([0.0] * self.data_length) + + self.temp_plot = self.widget_obj.temp_plot.plot(y=self.temp_data,clickable=True) + self.output_plot = self.widget_obj.output_plot.plot(y=self.output_data,clickable=True) + self.error_plot = self.widget_obj.error_plot.plot(y=self.error_data,clickable=True) + + self.widget_obj.setpointBox.valueChanged.connect(self.setpointChanged) + self.widget_obj.kpBox.valueChanged.connect(self.kpChanged) + self.widget_obj.kiBox.valueChanged.connect(self.kiChanged) + self.widget_obj.kdBox.valueChanged.connect(self.kdChanged) + self.widget_obj.PIDenableButton.clicked.connect(self.PIDenableButtonChanged) + self.widget_obj.OutputButton.clicked.connect(self.OutputButtonChanged) + + + def update(self): + if globalvars.conStatus: + TCP.send_cmd(globalvars.sock, 'CMD:\tGET_RUN_VALUES\t'+str(self.channel_num)+'\t') + msg = TCP.recv_msg(globalvars.sock) + + #ans = str2num(msg) + try: + ans = ast.literal_eval(msg) + except ValueError: + ans = [100, 0, 0] + + + self.temp_data = np.roll(self.temp_data, -1) + self.temp_data[self.data_length-1] = ans[0] + self.temp_field.setText("{:.4f}".format(ans[0])) + + self.error_data = np.roll(self.error_data, -1) + self.error_data[self.data_length-1] = ans[1] + + self.output_data = np.roll(self.output_data, -1) + self.output_data[self.data_length-1] = ans[2] + self.out_field.setText("{:.4f}".format(ans[2])) + + if self.tabNumber == self.activTabWidget.currentIndex(): + self.temp_plot.setData(y=self.temp_data) + self.error_plot.setData(y=self.error_data) + self.output_plot.setData(y=self.output_data) + + + def setpointChanged(self): + if globalvars.conStatus: + TCP.send_cmd(globalvars.sock, 'CMD:\tSET_SETPOINT\t'+str(self.channel_num)+'\t'+str(self.widget_obj.setpointBox.value())+'\t') + ans = TCP.recv_msg(globalvars.sock) + print(ans) + globalvars.settings["ch{0}".format(self.channel_num)]["setpoint"] = self.widget_obj.setpointBox.value() + + def kpChanged(self): + if globalvars.conStatus: + TCP.send_cmd(globalvars.sock, 'CMD:\tSET_KP\t'+str(self.channel_num)+'\t'+str(self.widget_obj.kpBox.value())+'\t') + ans = TCP.recv_msg(globalvars.sock) + print(ans) + + def kiChanged(self): + if globalvars.conStatus: + TCP.send_cmd(globalvars.sock, 'CMD:\tSET_KI\t'+str(self.channel_num)+'\t'+str(self.widget_obj.kiBox.value())+'\t') + ans = TCP.recv_msg(globalvars.sock) + print(ans) + + def kdChanged(self): + if globalvars.conStatus: + TCP.send_cmd(globalvars.sock, 'CMD:\tSET_KD\t'+str(self.channel_num)+'\t'+str(self.widget_obj.kdBox.value())+'\t') + ans = TCP.recv_msg(globalvars.sock) + print(ans) + + def PIDenableButtonChanged(self): + if globalvars.conStatus: + if self.widget_obj.PIDenableButton.isChecked(): + self.widget_obj.PIDenableButton.setStyleSheet("background-color: rgb(0,255,0)") + self.widget_obj.OutputButton.setEnabled(False) + TCP.send_cmd(globalvars.sock, 'CMD:\tLOCK\t'+str(self.channel_num)+'\t') + ans = TCP.recv_msg(globalvars.sock) + print(ans) + self.enable_field.setStyleSheet("background-color: rgb(0,255,0)") + else: + self.widget_obj.PIDenableButton.setStyleSheet("background-color: rgb(255,0,0)") + self.widget_obj.OutputButton.setEnabled(True) + TCP.send_cmd(globalvars.sock, 'CMD:\tIDLE\t'+str(self.channel_num)+'\t') + ans = TCP.recv_msg(globalvars.sock) + print(ans) + self.enable_field.setStyleSheet("background-color: rgb(255,0,0)") + else: + self.widget_obj.PIDenableButton.setChecked(False) + + def OutputButtonChanged(self): + if globalvars.conStatus: + TCP.send_cmd(globalvars.sock, 'CMD:\tSET_VOLTAGE\t'+str(self.channel_num)+'\t'+str(self.widget_obj.outputBox.value())+'\t') + ans = TCP.recv_msg(globalvars.sock) + print(ans) + + def setEnabled(self, val): + self.widget_obj.setEnabled(val) + + def loadSettings(self): + if globalvars.remoteSettings: + TCP.send_cmd(globalvars.sock,"CMD:\tGET_SETPOINT\t{0}\t".format(self.channel_num)) + setpoint = float(TCP.recv_msg(globalvars.sock)) + self.widget_obj.setpointBox.blockSignals(True) + self.widget_obj.setpointBox.setValue(setpoint) + self.widget_obj.setpointBox.blockSignals(False) + + TCP.send_cmd(globalvars.sock,"CMD:\tGET_KP\t{0}\t".format(self.channel_num)) + kp = float(TCP.recv_msg(globalvars.sock)) + self.widget_obj.kpBox.blockSignals(True) + self.widget_obj.kpBox.setValue(kp) + self.widget_obj.kpBox.blockSignals(False) + + TCP.send_cmd(globalvars.sock,"CMD:\tGET_KI\t{0}\t".format(self.channel_num)) + ki = float(TCP.recv_msg(globalvars.sock)) + self.widget_obj.kiBox.blockSignals(True) + self.widget_obj.kiBox.setValue(ki) + self.widget_obj.kiBox.blockSignals(False) + + TCP.send_cmd(globalvars.sock,"CMD:\tGET_KD\t{0}\t".format(self.channel_num)) + kd = float(TCP.recv_msg(globalvars.sock)) + self.widget_obj.kdBox.blockSignals(True) + self.widget_obj.kdBox.setValue(kd) + self.widget_obj.kdBox.blockSignals(False) + + TCP.send_cmd(globalvars.sock,"CMD:\tGET_PID_STATUS\t{0}\t".format(self.channel_num)) + enabled = int(TCP.recv_msg(globalvars.sock)) + print(enabled) + self.widget_obj.PIDenableButton.setChecked(enabled) + if self.widget_obj.PIDenableButton.isChecked(): + self.widget_obj.PIDenableButton.setStyleSheet("background-color: rgb(0,255,0)") + self.widget_obj.OutputButton.setEnabled(False) + self.enable_field.setStyleSheet("background-color: rgb(0,255,0)") + else: + self.widget_obj.PIDenableButton.setStyleSheet("background-color: rgb(255,0,0)") + self.widget_obj.OutputButton.setEnabled(True) + self.enable_field.setStyleSheet("background-color: rgb(255,0,0)") + + else: + #Set settings from file and trigger sending to server + self.widget_obj.setpointBox.setValue(float(globalvars.settings["ch{0}".format(self.channel_num)]["setpoint"])) + self.widget_obj.kpBox.setValue(float(globalvars.settings["ch{0}".format(self.channel_num)]["kp"])) + self.widget_obj.kiBox.setValue(float(globalvars.settings["ch{0}".format(self.channel_num)]["ki"])) + self.widget_obj.kdBox.setValue(float(globalvars.settings["ch{0}".format(self.channel_num)]["kd"])) + self.widget_obj.PIDenableButton.setChecked(bool(globalvars.settings["ch{0}".format(self.channel_num)]["PIDenabled"])) diff --git a/src/gui-client/channelInfoWidgetClass.py b/src/gui-client/channelInfoWidgetClass.py new file mode 100644 index 0000000..e799884 --- /dev/null +++ b/src/gui-client/channelInfoWidgetClass.py @@ -0,0 +1,24 @@ +from PyQt5.QtWidgets import QWidget +import pyqtgraph as pg + +from channel import Ui_Form + +class channelInfoWidget(QWidget, Ui_Form): + def __init__(self, parent): + QWidget.__init__(self) + self.setupUi(self) + + self.temp_plot = pg.PlotWidget() + self.temp_layout.addWidget(self.temp_plot) + self.temp_plot.setLabel('left', "temp", units='°C') + self.temp_plot.setLabel('bottom', "time")#, units='V') + + self.error_plot = pg.PlotWidget() + self.error_layout.addWidget(self.error_plot) + self.error_plot.setLabel('left', "error", units='°C') + self.error_plot.setLabel('bottom', "time")#, units='V') + + self.output_plot = pg.PlotWidget() + self.output_layout.addWidget(self.output_plot) + self.output_plot.setLabel('left', "U", units='V') + self.output_plot.setLabel('bottom', "time")#, units='V') diff --git a/src/gui-client/client.py b/src/gui-client/client.py new file mode 100644 index 0000000..bb58001 --- /dev/null +++ b/src/gui-client/client.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Jan 15 14:30:51 2018 + +@author: jan +""" + +import sys +import time +from PyQt5 import QtCore +from PyQt5.QtWidgets import QApplication, QMainWindow +from PyQt5.QtCore import QTimer + +import socket +import os +import json + +import queue + +from main_window import Ui_MainWindow + +from channelInfoClass import channelInfo + +import globalvars +###################################################################### +#TCP/IP +HOST = '' #Symbolic name +PORT = 2000 + +###################################################################### + +#Function to convert byte string to int/float +def str2num(s): + try: + return int(s) + except ValueError: + try: + return float(s) + except ValueError: + return 0 + +class ApplicationWindow(QMainWindow, Ui_MainWindow): + def __init__(self, ): + QMainWindow.__init__(self) + self.setupUi(self) + + self.tcpqueue = queue.Queue(100) + + self.channel = [0] + + temp_field = [self.temp_ch0] + out_field = [self.out_ch0] + enable_field = [self.enable_ch0] + + #Open json settings file defined in globalvars + self.loadSettingsFile(globalvars.settingsFile) + + for ch in range(len(self.channel)): + self.channel[ch] = channelInfo(ch,temp_field[ch],out_field[ch],enable_field[ch], ch, self.tabWidget ,self) + self.channel[ch].setEnabled(False) + + self.layout_ch0.addWidget(self.channel[0].widget_obj) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_0),globalvars.settings["ch0"]["name"]) + #self.layout_ch1.addWidget(self.channel[1].widget_obj) + #self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_1),globalvars.settings["ch1"]["name"]) + #self.layout_ch2.addWidget(self.channel[2].widget_obj) + #self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2),globalvars.settings["ch2"]["name"]) + #self.layout_ch3.addWidget(self.channel[3].widget_obj) + #self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3),globalvars.settings["ch3"]["name"]) + + self.ip_address.setText(globalvars.settings["defaultIP"]) + self.portField.setText(str(globalvars.settings["defaultPort"])) + + #Connect radio buttons to functions + self.remoteRadioButton.toggled.connect(lambda:self.settingsSelection(self.remoteRadioButton)) + self.localRadioButton.toggled.connect(lambda:self.settingsSelection(self.localRadioButton)) + + # Close event to save settings + app.aboutToQuit.connect(self.closeEvent) + # Button functions + self.connectButton.clicked.connect(self.connectButtonChange) + + self.refresh_timer = QTimer() + self.refresh_timer.timeout.connect(self.refresh) + + def connectButtonChange(self): + if self.connectButton.isChecked(): + #Open connection to server + self.sock = socket.socket() + self.address = self.ip_address.text() + self.port = self.portField.text() + + try: + self.sock.connect((self.address,int(self.port))) + except Exception as e: + print("something's wrong with %s:%s. Exception is %s" % (self.address, self.port, e)) + globalvars.conStatus = False + self.connectButton.setChecked(False) + return + + try: + data_recv = self.sock.recv(1024) + print(data_recv.decode()) + except OSError: + globalvars.conStatus = False + self.connectButton.setChecked(False) + print('\nSocket broken') + return + + globalvars.conStatus = True + globalvars.sock = self.sock + print("connect to "+ self.address) + + for ch in range(len(self.channel)): + self.channel[ch].loadSettings() + + self.refresh_timer.start(500) + + else: + globalvars.conStatus = False + self.refresh_timer.stop() + time.sleep(0.1) + print("disconnect from "+ self.address) + self.sock.close() + + for ch in range(len(self.channel)): + self.channel[ch].setEnabled(globalvars.conStatus) + + self.remoteRadioButton.setEnabled(not globalvars.conStatus) + self.localRadioButton.setEnabled(not globalvars.conStatus) + + def refresh(self): + for ch in range(len(self.channel)): + pass + self.channel[ch].update() + + def settingsSelection(self,b): + if b.objectName() == "remoteRadioButton": + if b.isChecked() == True: + globalvars.remoteSettings = True + else: + globalvars.remoteSettings = False + + if b.objectName() == "localRadioButton": + if b.isChecked() == True: + globalvars.remoteSettings = False + else: + globalvars.remoteSettings = True + + def loadSettingsFile(self,path): + with open(path) as c: + globalvars.settings = json.load(c) + + def closeEvent(self, event): + globalvars.conStatus = False + + print("Saving settings... ") + with open(globalvars.settingsFile, 'w') as f: + json.dump(globalvars.settings, f, ensure_ascii=True) + print('done.') + sys.exit(0) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + main = ApplicationWindow() + main.setWindowTitle("Tempertur-Controller V1") + main.show() + app.exec_() diff --git a/src/gui-client/globalvars.py b/src/gui-client/globalvars.py new file mode 100644 index 0000000..45abc74 --- /dev/null +++ b/src/gui-client/globalvars.py @@ -0,0 +1,6 @@ + +conStatus = False +sock = 0 +settings = 0 +remoteSettings = True +settingsFile = './settings.json' diff --git a/src/gui-client/main_window.py b/src/gui-client/main_window.py new file mode 100644 index 0000000..34f646c --- /dev/null +++ b/src/gui-client/main_window.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'main_window.ui' +# +# Created by: PyQt5 UI code generator 5.14.1 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(881, 742) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName("gridLayout_2") + self.groupBox = QtWidgets.QGroupBox(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) + self.groupBox.setSizePolicy(sizePolicy) + self.groupBox.setMinimumSize(QtCore.QSize(220, 0)) + self.groupBox.setMaximumSize(QtCore.QSize(157, 16777215)) + self.groupBox.setObjectName("groupBox") + self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox) + self.gridLayout_3.setObjectName("gridLayout_3") + self.label_5 = QtWidgets.QLabel(self.groupBox) + self.label_5.setObjectName("label_5") + self.gridLayout_3.addWidget(self.label_5, 0, 1, 1, 1) + self.connectButton = QtWidgets.QPushButton(self.groupBox) + self.connectButton.setCheckable(True) + self.connectButton.setObjectName("connectButton") + self.gridLayout_3.addWidget(self.connectButton, 5, 0, 1, 3) + self.localRadioButton = QtWidgets.QRadioButton(self.groupBox) + self.localRadioButton.setObjectName("localRadioButton") + self.gridLayout_3.addWidget(self.localRadioButton, 4, 0, 1, 3) + self.remoteRadioButton = QtWidgets.QRadioButton(self.groupBox) + self.remoteRadioButton.setChecked(True) + self.remoteRadioButton.setObjectName("remoteRadioButton") + self.gridLayout_3.addWidget(self.remoteRadioButton, 3, 0, 1, 3) + self.label = QtWidgets.QLabel(self.groupBox) + self.label.setObjectName("label") + self.gridLayout_3.addWidget(self.label, 0, 0, 1, 1) + self.ip_address = QtWidgets.QLineEdit(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.ip_address.sizePolicy().hasHeightForWidth()) + self.ip_address.setSizePolicy(sizePolicy) + self.ip_address.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.ip_address.setAlignment(QtCore.Qt.AlignCenter) + self.ip_address.setObjectName("ip_address") + self.gridLayout_3.addWidget(self.ip_address, 1, 0, 1, 1) + self.portField = QtWidgets.QLineEdit(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.portField.sizePolicy().hasHeightForWidth()) + self.portField.setSizePolicy(sizePolicy) + self.portField.setMaximumSize(QtCore.QSize(50, 16777215)) + self.portField.setObjectName("portField") + self.gridLayout_3.addWidget(self.portField, 1, 1, 1, 2) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_3.addItem(spacerItem, 6, 0, 1, 1) + self.gridLayout_2.addWidget(self.groupBox, 0, 0, 1, 1) + self.groupBox_2 = QtWidgets.QGroupBox(self.centralwidget) + self.groupBox_2.setObjectName("groupBox_2") + self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox_2) + self.gridLayout_4.setObjectName("gridLayout_4") + self.label_2 = QtWidgets.QLabel(self.groupBox_2) + self.label_2.setObjectName("label_2") + self.gridLayout_4.addWidget(self.label_2, 0, 0, 1, 1) + self.out_ch1 = QtWidgets.QLineEdit(self.groupBox_2) + self.out_ch1.setObjectName("out_ch1") + self.gridLayout_4.addWidget(self.out_ch1, 2, 1, 1, 1) + self.label_3 = QtWidgets.QLabel(self.groupBox_2) + self.label_3.setObjectName("label_3") + self.gridLayout_4.addWidget(self.label_3, 0, 2, 1, 1) + self.temp_ch2 = QtWidgets.QLineEdit(self.groupBox_2) + self.temp_ch2.setObjectName("temp_ch2") + self.gridLayout_4.addWidget(self.temp_ch2, 3, 0, 1, 1) + self.out_ch2 = QtWidgets.QLineEdit(self.groupBox_2) + self.out_ch2.setObjectName("out_ch2") + self.gridLayout_4.addWidget(self.out_ch2, 3, 1, 1, 1) + self.out_ch3 = QtWidgets.QLineEdit(self.groupBox_2) + self.out_ch3.setObjectName("out_ch3") + self.gridLayout_4.addWidget(self.out_ch3, 4, 1, 1, 1) + self.temp_ch0 = QtWidgets.QLineEdit(self.groupBox_2) + self.temp_ch0.setObjectName("temp_ch0") + self.gridLayout_4.addWidget(self.temp_ch0, 1, 0, 1, 1) + self.enable_ch3 = QtWidgets.QPushButton(self.groupBox_2) + self.enable_ch3.setStyleSheet("background-color: rgb(255,0,0)") + self.enable_ch3.setText("") + self.enable_ch3.setObjectName("enable_ch3") + self.gridLayout_4.addWidget(self.enable_ch3, 4, 2, 1, 1) + self.temp_ch1 = QtWidgets.QLineEdit(self.groupBox_2) + self.temp_ch1.setObjectName("temp_ch1") + self.gridLayout_4.addWidget(self.temp_ch1, 2, 0, 1, 1) + self.enable_ch2 = QtWidgets.QPushButton(self.groupBox_2) + self.enable_ch2.setStyleSheet("background-color: rgb(255,0,0)") + self.enable_ch2.setText("") + self.enable_ch2.setObjectName("enable_ch2") + self.gridLayout_4.addWidget(self.enable_ch2, 3, 2, 1, 1) + self.temp_ch3 = QtWidgets.QLineEdit(self.groupBox_2) + self.temp_ch3.setObjectName("temp_ch3") + self.gridLayout_4.addWidget(self.temp_ch3, 4, 0, 1, 1) + self.label_4 = QtWidgets.QLabel(self.groupBox_2) + self.label_4.setObjectName("label_4") + self.gridLayout_4.addWidget(self.label_4, 0, 1, 1, 1) + self.out_ch0 = QtWidgets.QLineEdit(self.groupBox_2) + self.out_ch0.setObjectName("out_ch0") + self.gridLayout_4.addWidget(self.out_ch0, 1, 1, 1, 1) + self.enable_ch0 = QtWidgets.QPushButton(self.groupBox_2) + self.enable_ch0.setStyleSheet("background-color: rgb(255,0,0)") + self.enable_ch0.setText("") + self.enable_ch0.setCheckable(False) + self.enable_ch0.setChecked(False) + self.enable_ch0.setObjectName("enable_ch0") + self.gridLayout_4.addWidget(self.enable_ch0, 1, 2, 1, 1) + self.enable_ch1 = QtWidgets.QPushButton(self.groupBox_2) + self.enable_ch1.setStyleSheet("background-color: rgb(255,0,0)") + self.enable_ch1.setText("") + self.enable_ch1.setObjectName("enable_ch1") + self.gridLayout_4.addWidget(self.enable_ch1, 2, 2, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_4.addItem(spacerItem1, 5, 0, 1, 1) + self.gridLayout_2.addWidget(self.groupBox_2, 0, 1, 1, 1) + self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) + self.tabWidget.setObjectName("tabWidget") + self.tab_0 = QtWidgets.QWidget() + self.tab_0.setObjectName("tab_0") + self.layout_ch0 = QtWidgets.QHBoxLayout(self.tab_0) + self.layout_ch0.setObjectName("layout_ch0") + self.tabWidget.addTab(self.tab_0, "") + self.tab_1 = QtWidgets.QWidget() + self.tab_1.setObjectName("tab_1") + self.layout_ch1 = QtWidgets.QHBoxLayout(self.tab_1) + self.layout_ch1.setObjectName("layout_ch1") + self.tabWidget.addTab(self.tab_1, "") + self.tab_2 = QtWidgets.QWidget() + self.tab_2.setObjectName("tab_2") + self.layout_ch2 = QtWidgets.QHBoxLayout(self.tab_2) + self.layout_ch2.setObjectName("layout_ch2") + self.tabWidget.addTab(self.tab_2, "") + self.tab_3 = QtWidgets.QWidget() + self.tab_3.setObjectName("tab_3") + self.layout_ch3 = QtWidgets.QHBoxLayout(self.tab_3) + self.layout_ch3.setObjectName("layout_ch3") + self.tabWidget.addTab(self.tab_3, "") + self.gridLayout_2.addWidget(self.tabWidget, 1, 0, 1, 2) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 881, 22)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.groupBox.setTitle(_translate("MainWindow", "Connection setup")) + self.label_5.setText(_translate("MainWindow", "Port:")) + self.connectButton.setText(_translate("MainWindow", "Connect")) + self.localRadioButton.setText(_translate("MainWindow", "use local settings")) + self.remoteRadioButton.setText(_translate("MainWindow", "use remote settings")) + self.label.setText(_translate("MainWindow", "IP:")) + self.ip_address.setText(_translate("MainWindow", "134.96.13.239")) + self.portField.setText(_translate("MainWindow", "2000")) + self.groupBox_2.setTitle(_translate("MainWindow", "Overview")) + self.label_2.setText(_translate("MainWindow", "temperatures (°C)")) + self.label_3.setText(_translate("MainWindow", "PID enabled")) + self.label_4.setText(_translate("MainWindow", "outputs (V)")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_0), _translate("MainWindow", "Channel 0")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_1), _translate("MainWindow", "Channel 1")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("MainWindow", "Channel 2")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("MainWindow", "Channel 3")) diff --git a/src/gui-client/main_window.ui b/src/gui-client/main_window.ui new file mode 100644 index 0000000..ef194ff --- /dev/null +++ b/src/gui-client/main_window.ui @@ -0,0 +1,300 @@ + + + MainWindow + + + + 0 + 0 + 881 + 742 + + + + MainWindow + + + + + + + + 0 + 0 + + + + + 220 + 0 + + + + + 157 + 16777215 + + + + Connection setup + + + + + + Port: + + + + + + + Connect + + + true + + + + + + + use local settings + + + + + + + use remote settings + + + true + + + + + + + IP: + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + 134.96.13.239 + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + 2000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Overview + + + + + + temperatures (°C) + + + + + + + + + + PID enabled + + + + + + + + + + + + + + + + + + + background-color: rgb(255,0,0) + + + + + + + + + + + + + background-color: rgb(255,0,0) + + + + + + + + + + + + + outputs (V) + + + + + + + + + + background-color: rgb(255,0,0) + + + + + + false + + + false + + + + + + + background-color: rgb(255,0,0) + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + + + + Channel 0 + + + + + + Channel 1 + + + + + + Channel 2 + + + + + + Channel 3 + + + + + + + + + + + 0 + 0 + 881 + 22 + + + + + + + + diff --git a/src/gui-client/pin.py b/src/gui-client/pin.py new file mode 100644 index 0000000..a124f40 --- /dev/null +++ b/src/gui-client/pin.py @@ -0,0 +1,129 @@ +from random import random +import json + +class InputOutputError(Exception): + pass + +conf={} +TEST='test' +conf[TEST]=False +IN=1 +OUT=0 +HIGH=1 +LOW=0 +BCM=0 +BOARD=1 +pins={} +out={} +values={} + +def config(path): + global conf + with open(path) as c: + conf = json.load(c) + if not conf[TEST]: + global RPi + global GPIO + import RPi.GPIO as GPIO + +#TODO check if initial is discarded for input or error is raised +def setup(channel,in_out,initial=HIGH): + global conf + if type(channel) is list: + for el in channel: + _setup_one(el,in_out,initial) + else: + _setup_one(channel,in_out,initial) + +def _setup_one(channel,in_out,initial): + if conf[TEST]: + pins[channel]=in_out + else: + if initial == HIGH: + initial = GPIO.HIGH + else: + initial = GPIO.LOW + if in_out == IN: + GPIO.setup(channel,GPIO.IN) # initial not a valid parameter for input, GPIO error + else: + GPIO.setup(channel,GPIO.OUT,initial=initial) + +def check_in_out(channel,in_out): + try: + if not pins[channel]==in_out: + raise InputOutputError("Wrong confuration for channel {}! You're treating an input channel as output or vice versa.") + except KeyError: + raise InputOutputError("Wrong confuration for channel {}! setup() not called for this channel before calling input() or output().") + +def setmode(mode): + if conf[TEST]: + pass + else: + if mode == BCM: + GPIO.setmode(GPIO.BCM) + else: + GPIO.setmode(GPIO.BOARD) + +def input(channel): + if conf[TEST]: + check_in_out(channel,IN) + try: + values[channel] + except KeyError: + return random() + return values[channel] + else: + return GPIO.input(channel) + +def output(channel,value): + if type(channel) is list: + for el in channel: + _output_one(el,value) + else: + _output_one(channel,value) + +def _output_one(channel,value): + if conf[TEST]: + check_in_out(channel,OUT) + out[channel]=value + else: + GPIO.output(channel,value) + +def set_value(channel,value): + values[channel]=value + +def get_output(channel): + return out[channel] + +def cleanup(channel=None): + global pins,out,values + if channel==None: + if conf[TEST]: + pins,out,values={},{},{} + else: + GPIO.cleanup() + else: + if type(channel) is list or type(channel) is tuple: + for el in channel: + _cleanup_one(el) + else: + _cleanup_one(channel) + +def _cleanup_one(channel): + global pins,out,values + if conf[TEST]: + if channel in values.keys() and pins[channel]==IN: + del values[channel] + elif channel in out.keys(): + del out[channel] + if channel in pins.keys(): + del pins[channel] + else: + GPIO.cleanup(channel) + +def setwarnings(val): + if conf[TEST]: + pass + else: + GPIO.setwarnings(val) + diff --git a/src/gui-client/runGui.bat b/src/gui-client/runGui.bat new file mode 100644 index 0000000..afdbfb5 --- /dev/null +++ b/src/gui-client/runGui.bat @@ -0,0 +1,11 @@ +:: Anaconda3 path +set root=C:\ProgramData\Anaconda3 + +:: Program path +set guiPath=C:\Users\Jan\Documents\Projekte\TimeBinControl\src\gui-client + +call %root%\Scripts\activate.bat %root% + +call cd /D %guiPath% + +python client.py \ No newline at end of file diff --git a/src/gui-client/settings.json b/src/gui-client/settings.json new file mode 100644 index 0000000..6d62712 --- /dev/null +++ b/src/gui-client/settings.json @@ -0,0 +1 @@ +{"defaultIP": "134.96.13.250", "defaultPort": 2000, "ch0": {"name": "Encoder", "setpoint": 26.62, "kp": 12.0, "ki": 4.0, "kd": 0, "PIDenabled": 0, "channelEnabled": 1}, "ch1": {"name": "Decoder", "setpoint": 26.62, "kp": 12.0, "ki": 4.0, "kd": 0, "PIDenabled": 0, "channelEnabled": 1}, "ch2": {"name": "Channel 3", "setpoint": 34.05, "kp": 0.0, "ki": 0.0, "kd": 0.0, "PIDenabled": 0, "channelEnabled": 1}, "ch3": {"name": "Channel 4", "setpoint": 21.5, "kp": 0.0, "ki": 0.0, "kd": 0.0, "PIDenabled": 0, "channelEnabled": 1}} \ No newline at end of file diff --git a/src/gui-client/tcptools.py b/src/gui-client/tcptools.py new file mode 100644 index 0000000..4d343f2 --- /dev/null +++ b/src/gui-client/tcptools.py @@ -0,0 +1,31 @@ +import struct +import socket + +def send_msg(sock, msg): + # Prefix each message with a 4-byte length (network byte order) + msg = str(msg) + msg = struct.pack('>I', len(msg)) + msg.encode() + sock.sendall(msg) + +def send_cmd(sock, msg): + msg = msg + ' '*(1024 - len(msg)) + sock.send((msg.encode())) + +def recv_msg(sock): + # Read message length and unpack it into an integer + raw_msglen = sock.recv(4) + if not raw_msglen: + return None + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return recvall(sock, msglen) + +def recvall(sock, n): + # Helper function to recv n bytes or return None if EOF is hit + data = '' + while len(data.encode()) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data = packet.decode() + return data diff --git a/src/server.py b/src/server.py new file mode 100644 index 0000000..e8de77e --- /dev/null +++ b/src/server.py @@ -0,0 +1,147 @@ +import sys +import time +import socket +import threading + +import queue +import zmq + +import pin as GPIO +GPIO.config('./config.json') + +import MAX11270 as adc +import mcp4822 as dac +import PID as pid +import tcptools as TCP +import spithread as SPIThread +import clientthread as clientThread +import globalvars +###################################################################### +#TCP/IP +HOST = '' #Symbolic name +PORT = 2000 + +###################################################################### +# GPIO conf +GPIO.setmode(GPIO.BCM) +ADC_CLK_PIN = 21 +ADC_MOSI_PIN = 20 +ADC_MISO_PIN = 19 +ADC_CS_PIN = 17 #ADC 0 -> input 0,1 +ADC_SYNC_PIN = 27 +ADC_RSTB_PIN = 22 + +DAC_CLK_PIN = 11 +DAC_MOSI_PIN = 10 +DAC_CS_PIN = 8 +DAC_LDAC_PIN = 5 + + +#----nonglobals------- +# this is the array of threads +t = [] # this is the array of threads + +###################################################################### + +#Workaround for Windows to handle KeyboardInterrupt +def workaround(): + try: + while True: input() + except (KeyboardInterrupt, EOFError): + socket.socket().connect((HOST,PORT)) +###################################################################### +#start program + +# Setting up server +lock = threading.Lock() #create lock functionalities +print('\nCreate socket') +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#create socket +sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1) +print(' ...done') +print('\nBind socket...') +#bind socket +try: + sock.bind((HOST,PORT)) +except socket.error as msg: + print(' ...Bind failed. Error Code : '+str(msg.errno)+' Message : '+ msg.strerror) + sock.close() + sys.exit() +print(' ...done') +print('\nOpen socket listener...') +sock.listen(10) +print(' ...done') + +# Setting up I/O +adc0 = adc.MAX11270(ADC_CLK_PIN, ADC_MOSI_PIN, ADC_MISO_PIN, ADC_CS_PIN, ADC_SYNC_PIN, ADC_RSTB_PIN) + + +dac0 = dac.MCP4822(DAC_CLK_PIN, DAC_MOSI_PIN, DAC_CS_PIN, DAC_LDAC_PIN, nullVoltage=1.29) + + +pid_obj = [pid.PID(0, 0, 0, 1, 0)] + + +#Start spithread +#new_t = threading.Thread(target=SPIThread.spithread,args=([adc0, adc1], [dac0, dac1], SPIQueue, pidqueue)) +new_t = threading.Thread(target=SPIThread.spithread,args=([adc0], [dac0],)) +new_t.deamon = True +new_t.start() +t.append(new_t) #append to array of threads + +#Start pid thread +for i in range(len(pid_obj)): + new_t = threading.Thread(target=pid.run,args=(pid_obj[i], 100,i)) + new_t.deamon = True + new_t.start() + t.append(new_t) #append to array of threads + +#Start workaround only on Windows +if sys.platform.startswith('win'): + new_t = threading.Thread(target=workaround,args=()) + new_t.deamon = True + new_t.start() + t.append(new_t) #append to array of threads + +print('\nStart waiting for connections...') +while True: + conn = None + try: + #wait to accept a connection - blocking call + print('\nWait for connection...') + conn, addr = sock.accept() + print('Connected with ' + addr[0] + ':' + str(addr[1])) + print('\nOpen client thread...') + new_t = threading.Thread(target=clientThread.clientthread,args=(conn,)) + new_t.deamon = True + new_t.start() + t.append(new_t) #append to array of threads + print(' ...done') + print('\nCleanup threads...') + t = [this_t for this_t in t if this_t.is_alive()]#cleanup t + print(' ...done') + + except (KeyboardInterrupt,SystemExit): #Ctrl + break for keyboard interrupt (windows) + print('\nKeyboard interrupt...') + print('\nCleanup threads...') + t = [this_t for this_t in t if this_t.is_alive()]#cleanup t + print(' ...done') + print('\nClosing socket...') + sock.close() + print(' ...done') + print('\nWaiting for threads to close...') + globalvars.stopall = True + + #Send end command to all threads + #SPIQueue.put(['END']) + #Connect to SPI thread via zmq tcp socket + context = zmq.Context() + SPIclient = context.socket(zmq.REQ) + SPIclient.connect("tcp://localhost:5555") + SPIclient.send("END\t".encode()) + msg = SPIclient.recv() + + for this_t in t: #wait for threads to close + this_t.join() + print(' ...done') + GPIO.cleanup() + sys.exit() diff --git a/src/spithread.py b/src/spithread.py new file mode 100644 index 0000000..1776d42 --- /dev/null +++ b/src/spithread.py @@ -0,0 +1,112 @@ +import queue +import zmq +import math +import MAX11270 as adc +import MCP4822 as dac + +import globalvars + +def adc2T(dU): + #constants + #VREF = (REF+) – (REF–) + #Vref = 2.5 + #wheatstonebridge + # U0_____ + # | + # R0 + # Uw_____|__ + # | | + # R2 R4 + # |__dU__| + # | | + # R1 R3 + # |______| + # Gnd_____|_ + # + # R1 = resistance of thermistor + R0 = 0 + R2 = 5100 #(Ohm) + R3 = 97600 #(Ohm) + R4 = 5100 #(Ohm) + U0 = 3 #(V) + #steinhart-Hart Coefficients 10uA + # NTC_A = 9.7142e-4; + # NTC_B = 2.3268e-4; + # NTC_C = 8.0591e-8; + #steinhart-Hart Coefficients 100uA + NTC_A = 9.6542e-4 + NTC_B = 2.3356e-4 + NTC_C = 7.7781e-8 + + R1 = -(R0*R2*dU - R2*R3*U0 + R0*R3*dU + R0*R4*dU + R2*R3*dU + R2*R4*dU)/(R4*U0 + R0*dU + R3*dU + R4*dU) + #calculate temperature + try: + T = 1/(NTC_A + NTC_B*math.log(float(R1)) + NTC_C*math.pow(math.log(float(R1)),3)) - 273.15 + except ValueError: + T = 6666 + return T + +#Thread for handling communication with spi devices +#def spithread(adc, dac,in_queue,out_queue): +def spithread(adc, dac): + context = zmq.Context() + server = context.socket(zmq.REP) + server.bind("tcp://*:5555") + + stop = False + while not globalvars.stopall and not stop: + cmd_recv = server.recv() #Blocking call, wait for a new message + + ans = 'NAK' + msg = cmd_recv.decode().split('\t') + #msg = in_queue.get() #Blocking call, waits for an element + try: + msg[1] = int(msg[1]) + msg[2] = int(msg[2]) + msg[3] = int(msg[3]) + except (IndexError, ValueError): + pass + + if msg[0] == 'ADC': + if msg[1] <= 1: + if msg[2] <= 1: + try: + T = adc2T(adc[msg[1]].read_differential()) + except TimeoutError: + print("Conversion takes to long!") + T = 100 + if msg[3] <= 5: + ans = str(T) + #out_queue[msg[3]].put(T) + else: + print('Error: Invalid output queue!') + else: + print('Error: Invalid device channel!') + else: + print('Error: Invalid device number!') + + elif msg[0] == 'DAC': + if msg[1] <= 1: + if msg[2] <= 1: + if msg[2] == 1: + voltage = float(msg[4]) - dac[msg[1]].nullVoltage + T = dac[msg[1]].write(voltage,msg[2]) + else: + T = dac[msg[1]].write(float(msg[4]),msg[2]) + if msg[3] <= 5: + #out_queue[msg[3]].put('ACK') + ans = 'ACK' + else: + print('Error: Invalid output queue!') + else: + print('Error: Invalid device channel!') + else: + print('Error: Invalid device number!') + elif msg[0] == 'END': + ans = 'ACK' + stop = True + else: + print('Error: Invalid device!') + + #zmq ensure that the answer is delivered to the right clientthread + server.send(ans.encode()) diff --git a/src/tcptools.py b/src/tcptools.py new file mode 100644 index 0000000..4d343f2 --- /dev/null +++ b/src/tcptools.py @@ -0,0 +1,31 @@ +import struct +import socket + +def send_msg(sock, msg): + # Prefix each message with a 4-byte length (network byte order) + msg = str(msg) + msg = struct.pack('>I', len(msg)) + msg.encode() + sock.sendall(msg) + +def send_cmd(sock, msg): + msg = msg + ' '*(1024 - len(msg)) + sock.send((msg.encode())) + +def recv_msg(sock): + # Read message length and unpack it into an integer + raw_msglen = sock.recv(4) + if not raw_msglen: + return None + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return recvall(sock, msglen) + +def recvall(sock, n): + # Helper function to recv n bytes or return None if EOF is hit + data = '' + while len(data.encode()) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data = packet.decode() + return data