Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

tcpsocket not sending multiple messages



  • Hi,
    I'm trying to make a socket based app that that contains two functions:
    1- upon establishing the connection between the client and the server, the client should send the system time to the server and the server app will use this value to set the system time on the server machine
    2- upon request from the client the server should trigger a routine to open a gate(this is done through raspberrypi GPIO pins)

    I'm having trouble sending multiple messages on the same socket, the first message that the client send(the time setting message) will be delivered ok, but the subsequent messages(opening the gate commands) will not reach the server and the client shows this error

    QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread
    

    I've created a small app to show the problem:
    1- the server is just a window with two lables: time and gatestate, upon receiving the message from the client it will show the time in the time Qlabel and shows "the gate is open" if it receive that message from the client( the gate will close after a few seconds)
    2-the client only contains a button that sends the command to open the gate, but before that it will send a time setting command once the tcp connection is established

    full code is here:
    1-the server:

    
    import sys
    import os
    import signal
    import traceback
    from PyQt5 import QtWidgets as qtw
    from PyQt5 import QtCore as qtc
    from PyQt5 import QtGui as qtg
    from PyQt5.QtNetwork import QHostAddress, QTcpServer
    
    from PyQt5 import QtCore as qtc
    from PyQt5.QtNetwork import QHostAddress, QTcpServer
    
    
    class serversocket(qtc.QObject):
        open_gate = qtc.pyqtSignal()
        set_time = qtc.pyqtSignal(str)
        cleanup = qtc.pyqtSignal()
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, *kwargs)
            self.cleanup.connect(self.cleanup_slot)
            self.tcpServer = QTcpServer(self)
            #allow multiple clients to connect, store clients in the connectedclients variable
            self.connectedclients = []
            PORT = 8000
            if not self.tcpServer.listen(QHostAddress.SpecialAddress.AnyIPv4, PORT):
                print("cant listen!")
            self.tcpServer.newConnection.connect(self.on_newConnection)
    
        def readmessage(self, clientid):
            print("reading data from client")
    
            # read incomming data
            instr = self.connectedclients[clientid].readAll()
            print("data was read")
            # in this case we print to the terminal could update text of a widget if we wanted.
            recievedmsg= str(instr, encoding='utf-8')
            print("recieved message is: ",recievedmsg)
            if(recievedmsg == "open"):
                print("open gate")
                self.open_gate.emit()
            elif(recievedmsg.startswith("settime")):
                newtime = float(recievedmsg.split(":")[1])
                print("setting os time")
                self.set_time.emit(str(newtime))
    
    
        def on_newConnection(self):
            # Get a QTcpSocket from the QTcpServer
            print("got connection")
            #add the clinet to the connectedclients variable
            self.connectedclients.append(self.tcpServer.nextPendingConnection())
            clientid = len(self.connectedclients)-1
            self.connectedclients[clientid].readyRead.connect(lambda: self.readmessage(clientid))
    
        @qtc.pyqtSlot()
        def cleanup_slot(self):
            if (self.tcpServer.isListening()):
                self.tcpServer.close()
        @qtc.pyqtSlot(bool)
        def send_back_door_state(self, newstate):
            if(len(self.connectedclients) > 0):
                print("sending back our message")
                if(newstate is False):
                    message = bytes("unlocked",encoding="utf-8")
                else:
                    message = bytes("locked",encoding="utf-8")
                print("sending '{}' message".format(message))
                # get a byte array of the message encoded appropriately.
                for client in self.connectedclients:
                    client.write(message)
    
    
    
    
    class MainWindow(qtw.QWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.resize(400, 300)
            self.gridLayout = qtw.QGridLayout(self)
            self.gateState = qtw.QLabel(self)
            self.gateState.setText("gate is closed")
            self.gateState.setStyleSheet("background-color:red;")
            self.time = qtw.QLabel(self)
            self.time.setText("time has not been set")
            
            self.gridLayout.addWidget(self.time,1,0,1,1)
            self.gridLayout.addWidget(self.gateState,0,0,1,1)
           
    
            self.socket_worker = serversocket()
            self.socket_thread = qtc.QThread()
            self.socket_worker.moveToThread(self.socket_thread)
            self.socket_worker.open_gate.connect(self.open_gate)
            self.socket_worker.set_time.connect(self.time.setText)
            self.socket_thread.start()
    
            self.show()
        @qtc.pyqtSlot()
        def open_gate(self):
            self.gateState.setText("gate is opened")
            self.gateState.setStyleSheet("background-color:green;")
            qtc.QTimer.singleShot(2000,lambda:[self.gateState.setText("gate is closed"),self.gateState.setStyleSheet("background-color:red;")])
        def closeEvent(self, event):
            print("closing server control")
            self.cleanup()
            qtw.QApplication.closeAllWindows()
    
        def cleanup(self):
            self.socket_worker.cleanup.emit()
    
    
    
    def setup_interrupt_handling():
        """Setup handling of KeyboardInterrupt (Ctrl-C) for PyQt."""
        signal.signal(signal.SIGINT, _interrupt_handler)
        # Regularly run some (any) python code, so the signal handler gets a
        # chance to be executed:
        safe_timer(50, lambda: None)
    
    
    # Define this as a global function to make sure it is not garbage
    # collected when going out of scope:
    def _interrupt_handler(signum, frame):
        """Handle KeyboardInterrupt: quit application."""
        w.cleanup()
        qtw.QApplication.quit()
    
    
    def safe_timer(timeout, func, *args, **kwargs):
        """
        Create a timer that is safe against garbage collection and overlapping
        calls. See: http://ralsina.me/weblog/posts/BB974.html
        """
        def timer_event():
            try:
                func(*args, **kwargs)
            finally:
                qtc.QTimer.singleShot(timeout, timer_event)
        qtc.QTimer.singleShot(timeout, timer_event)
    
    
    def excepthook(exc_type, exc_value, exc_tb):
        tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
        print("error catched!:")
        print("error message:\n", tb)
        qtw.QApplication.quit()
        # or QtWidgets.QApplication.exit(0)
    
    
    if __name__ == '__main__':
        # os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
        
        sys.excepthook = excepthook
        # try:
        #     qtw.QApplication.setAttribute(qtc.Qt.AA_EnableHighDpiScaling)
        # except AttributeError:  # Attribute only exists for Qt>=5.6.
        #     pass
        app = qtw.QApplication(sys.argv)
        setup_interrupt_handling()
    
        w = MainWindow()
        # app.aboutToQuit(GPIO.cleanup())
        ret = app.exec_()
        sys.exit(ret)
    

    2- the client:

    
    import sys
    import os
    import signal
    import traceback
    from PyQt5 import QtWidgets as qtw
    from PyQt5 import QtCore as qtc
    from PyQt5 import QtGui as qtg
    from PyQt5 import QtSql
    
    
    import time
    from PyQt5 import QtCore as qtc
    from PyQt5.QtNetwork import QTcpSocket, QAbstractSocket
    
    class clientsocket(qtc.QObject):
        opengate = qtc.pyqtSignal()
        socketState = qtc.pyqtSignal(bool)
        gateState = qtc.pyqtSignal(bool)
        cleanup = qtc.pyqtSignal()
    
        def __init__(self, *args, **kargs):
            super().__init__(*args, *kargs)
            self.cleanup.connect(self.cleanup_slot)
            self.tcpSocket = QTcpSocket()
            self.tcpSocket.setSocketOption(QTcpSocket.KeepAliveOption, 1)
            self.blockSize = 0
            self.tcpSocket.readyRead.connect(self.on_read)
            self.tcpSocket.error.connect(self.displayError)
            self.tcpSocket.stateChanged.connect(self.state_changed)
            self.tcpSocket.connected.connect(self.on_connected)
            
            #each 2 seconds check if the socket is connected, if not try to connect
            self.connectingtimer = qtc.QTimer()
            self.connectingtimer.timeout.connect(self.connect)
            self.connectingtimer.start(2000)
            self.connect()
    
        @qtc.pyqtSlot(QTcpSocket.SocketState)
        def state_changed(self, newstate):
            print("state changed, new socket state:",newstate)
    
        @qtc.pyqtSlot()
        def connect(self):
            print("socket state: ",self.tcpSocket.state())
            if(self.tcpSocket.state() == 0):
                self.socketState.emit(False)
                print("tcpsocket connecting")
                try:
                    self.tcpSocket.connectToHost(
                        '127.0.0.1', 8000, qtc.QIODevice.ReadWrite)
                except Exception as e:
                    print("exception in socket connecting")
                    print(e)
                else:
                    if self.tcpSocket.state() == 3:
                        print("socket connected successfully")
                        self.socketState.emit(True)
    
        @qtc.pyqtSlot()
        def on_connected(self):
            # current_time = time.clock_gettime(time.CLOCK_REALTIME)
            current_time = time.time()
            timemsg = "settime:{}".format(int(current_time))
            print("setting gate time")
            self.tcpSocket.waitForConnected(1000)
            print("connected")
            print("socket state: ",self.tcpSocket.state())
            print("sending message '{}'".format(timemsg))
            self.tcpSocket.write(bytes(timemsg,encoding="utf-8"))
            print("sent '{}' message".format(timemsg))
            self.socketState.emit(True)
    
        def on_read(self):
            print("reading message from server")
            message = self.tcpSocket.readAll()
            print(message)
            if (message):
                if (message == "locked"):
                    print("gate locked")
                    self.gateState.emit(True)
                elif(message == "unlocked"):
                    self.gateState.emit(False)
                    print("gate unlocked")
    
        @qtc.pyqtSlot()
        def open_gate(self):
            print("open gate")
            self.tcpSocket.waitForConnected(1000)
            print("connected")
            print("socket state: ",self.tcpSocket.state())
            print("sending message \"open\"")
            self.tcpSocket.write(bytes("open",encoding="utf-8"))
    
            print("sent 'open' message")
        
        @qtc.pyqtSlot()
        def cleanup_slot(self):
            if (self.tcpSocket.state() == 3):
                self.tcpSocket.disconnect()
    
        @qtc.pyqtSlot(QAbstractSocket.SocketError)
        def displayError(self, socketError):
            print("socket errrrr")
            if socketError == QAbstractSocket.RemoteHostClosedError:
                pass
            else:
                print("The following error occurred: %s." %
                      self.tcpSocket.errorString())
    
    
    
    
    class MainWindow(qtw.QWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            self.gridLayout = qtw.QGridLayout(self)
            self.openGate = qtw.QPushButton("open the gate")
            self.openGate.setEnabled(False)
            self.openGate.setMinimumSize(qtc.QSize(50,50))
            self.gridLayout.addWidget(self.openGate,0,0,1,1)
    
            self.socket_worker = clientsocket()
            self.socket_thread = qtc.QThread()
            self.socket_worker.moveToThread(self.socket_thread)
            self.socket_worker.socketState.connect(self.openGate.setEnabled)
            self.socket_worker.gateState.connect(self.openGate.setEnabled)
            self.socket_thread.start()
    
    
            self.openGate.clicked.connect(self.socket_worker.open_gate)
            self.show()
    
        def closeEvent(self, event):
            print("closing client control")
            self.cleanup()
            qtw.QApplication.closeAllWindows()
    
        def cleanup(self):
            self.socket_worker.cleanup.emit()
            
    
    
    def setup_interrupt_handling():
        """Setup handling of KeyboardInterrupt (Ctrl-C) for PyQt."""
        signal.signal(signal.SIGINT, _interrupt_handler)
        # Regularly run some (any) python code, so the signal handler gets a
        # chance to be executed:
        safe_timer(50, lambda: None)
    
    
    # Define this as a global function to make sure it is not garbage
    # collected when going out of scope:
    def _interrupt_handler(signum, frame):
        """Handle KeyboardInterrupt: quit application."""
        w.cleanup()
        qtw.QApplication.quit()
    
    
    def safe_timer(timeout, func, *args, **kwargs):
        """
        Create a timer that is safe against garbage collection and overlapping
        calls. See: http://ralsina.me/weblog/posts/BB974.html
        """
        def timer_event():
            try:
                func(*args, **kwargs)
            finally:
                qtc.QTimer.singleShot(timeout, timer_event)
        qtc.QTimer.singleShot(timeout, timer_event)
    
    
    def excepthook(exc_type, exc_value, exc_tb):
        tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
        print("error catched!:")
        print("error message:\n", tb)
        qtw.QApplication.quit()
        # or QtWidgets.QApplication.exit(0)
    
    
    if __name__ == '__main__':
        os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
        
        sys.excepthook = excepthook
        try:
            qtw.QApplication.setAttribute(qtc.Qt.AA_EnableHighDpiScaling)
        except AttributeError:  # Attribute only exists for Qt>=5.6.
            pass
        app = qtw.QApplication(sys.argv)
        setup_interrupt_handling()
    
        w = MainWindow()
        # app.aboutToQuit(GPIO.cleanup())
        ret = app.exec_()
        sys.exit(ret)
    
    

  • Lifetime Qt Champion

    Hi,

    Why are you using threads ? Qt sockets are asynchronous so there's no need for that.

    In any case, you can't move sockets to different threads. They must be created in the thread they will be used in as the error message suggests.



  • @SGaist said in tcpsocket not sending multiple messages:

    Hi,
    Why are you using threads ? Qt sockets are asynchronous so there's no need for that.
    In any case, you can't move sockets to different threads. They must be created in the thread they will be used in as the error message suggests.

    Thank you, I didn't know that Qt sockets are asynchronous, removing the unneeded threading fixed the issue



  • @SGaist said in tcpsocket not sending multiple messages:

    Why are you using threads ? Qt sockets are asynchronous so there's no need for that.

    A quick question, are the sql database classes also asynchronous ? where can check if a class is asynchronous or not in the docs?

    thanks



  • @rhx9
    No, the database classes are not asynchronous.

    There isn't some simple statement about "this class is asynchronous". You have to read the documentation page. If you see signals for "such-and-such action finished" that will mean it is asynchronous --- your code continues after the call and the signal is emitted for you to place a slot on when the requested operation finishes. If it does not say that --- and perhaps the action method returns a result like, say, QSqlQuery::exec() does --- then you know it must be synchronous.


Log in to reply