Independent Socket server on NAO

For my latest project, the NAO for Scratch 2 extension, I wrote a network server that runs as NAO Choregraphe behavior on the robot.

As soon as the behavior is started, it listens permanently for an incoming connection until the behavior is stopped or the robot is shut down. When the client disconnects (gracefully or not), the server simply awaits a new connection. In this special case, I made my live simpler by allowing only 1 single concurrent connection. Having to deal with more parallel connections would invoke Thread management and that’s something I like to keep for later ;) In my use case, 1 connection is just fine.

So here is the graphical representation of the box:

SocketServerBox

The server behavior starts on the “OnStart” signal and sends an “OnStopped” when shut down. This means that the behavior is stopped and not that there is a disconnection. In any case, the sockets are closed properly inside the behavior.

Each time a client connects or disconnects, signals are sent via “OnConnect” or “OnDisconnect” respectively. This helps informing the user on what is going on.

Once connected,  the server awaits incoming command chains having the following format: “uid#command#param1#param2#…#paramN$“. “#” is the chunk separator, “uid” a unique command ID that is optionally used to inform the client that the command has finished to be executed (eg. think of an animation), “command” is the effective action command and “param1-N” an optional list of parameter values. “$” is the End Of Command control character. An example would be “123#say#Hello World!$“.

Using this simple pattern is a choice that allows us to test the server using a simple (raw-mode) telnet session. You may however send whatever you like depending on your application and use cases.

Once the command is received, it is transformed into a “string[10]” array and send over the “command” output to a handler box: “String[uid,command,param1,param2,…,paramN]“.

The choice to use an array to send the command data instead of separate output signals à la “command“, “parameter1“, “parameter2” etc., has been made to avoid race conditions on the signals. This avoids that, for example, the “command” signal is received with the “parameterX” signal still onits way. In Choregraphe, the order of reception is not guaranteed to be the same as the order of sending signals. The drawback of this solution is that the executor box needs to know the array structure, which is something that would not be required in a clean design (low coupling, high coherence). However, no choice in this case.

If a feedback to the client is desired, the executor box simply has to send the “string[10]” array back to the “OnCommandFinished” input to trigger the emission of a message of type “uid#end$” to the client.

The “uid” is always required since, even if we allow only 1 connected client at the same time, we permit to execute commands in parallel (like say and an animation). This means that the first command sent is not necessarily the first command to conclude.

And here is the code:

import socket

class MyClass(GeneratedClass):

    # ---------------------------------------------------------
    def __init__(self):
        GeneratedClass.__init__(self)
        self.listenSocket=None
        self.connectedSocket=None
        self.doingUnload=False
        pass

    # ---------------------------------------------------------
    def onLoad(self):
        pass

        
    # --------------------------------------------------------
    # Handles a single connection by receiving a message and decomposing it into fields,
    # which are then sent to an box output.
    #
    # Format incoming command string: "uid#command#param1#param2#...#paramN$",
    #
    # Format of the outgoing command array:
    #     String[uid,command,param1,param2,...,paramN]
    #
    def handleConnection(self):

        total_data=""

        try:
            while True:
                data = self.connectedSocket.recv(1)
                if not data:
                    self.logger.info("Socket "+str(self.connectedSocket)+" closed by peer.")
                    self.onDisconnect()
                    break
    
                if data=='$':
                    self.logger.info('Execute:'+total_data)
                    
                    # Convert the command string to an array of strings.
                    # We use an array instead of separate output
                    # signals to avoid race conditions on the 10 different signals
                    self.command( total_data.strip(' tnr').split('#') )
                    
                    total_data=""
                    
                else:
                    if len(total_data)<255:  # data buffer limitation
                        total_data += data
                    
        except Exception as ex:
            self.onDisconnect()
            self.logger.info("Exception while listening on socket "+str(self.connectedSocket)+", close connection! ")
            self.logger.info(ex)
            
        try:
            self.connectedSocket.close();
        except:
            pass
        
        self.connectedSocket=None
        pass    
        
        
    # ---------------------------------------------------------
    # Starts the TCP/IP listener on the configured port and delegates the connection to a
    # handler. No multiple connections supported so far to avoid complicating things with
    # multi-threading and synchronised data management.
    #
    def onInput_onStart(self):
        
        while True:
        
            # Open port for listening
            try:
                self.logger.info("Listening on port "+str(self.getParameter("port"))+" allowing 1 concurrent connection.")
                
                self.listenSocket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
                self.listenSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                self.listenSocket.bind(("",self.getParameter("port")))
                self.listenSocket.listen(1)
                self.connectedSocket,address=self.listenSocket.accept()  # -> waiting for incoming connection
            
            except Exception as ex:
                # Very probably triggered by UNLOAD by stopping the behaviour
                if self.doingUnload==False:
                    self.onUnload()  # |_ don't call it twice
                break
        
            # Handle incoming connection
            self.onConnect()
            self.logger.info('Connected to '+str(address)+"/"+str(self.connectedSocket))

            # Send back a welcome message. The ~ prefix indicates a small talk message (i.e. can be ignored by client)
            self.connectedSocket.sendall("~Hello, NAO4Scratch speaking! Waiting for your commands!nr")

            self.handleConnection()
    
        # |_ while True
        pass

    # ---------------------------------------------------------
    # Stop the socket server box.  Closes all sockets and set the onStop output.
    #
    def onUnload(self):
        self.logger.info('Unloading: closing all sockets')
        
        self.doingUnload=True
        
        try:
            self.connectedSocket.shutdown(socket.SHUT_RDWR)
            self.connectedSocket.close()  
        except:
            pass
            
        try:
            self.listenSocket.shutdown(socket.SHUT_RDWR)
            self.listenSocket.close()  
        except:
            pass
                    
        self.connectedSocket=None
        self.listenSocket=None
        
        self.onStopped()
        pass

    # ---------------------------------------------------------
    # External signal forcing to stop the box.
    #
    def onInput_onStop(self):
        self.onUnload()
        #~ it is recommended to call onUnload of this box in a onStop method,
        # as the code written in onUnload is used to stop the box as well
        pass
        
        
    # ---------------------------------------------------------
    # External signal when the blocking command has been finished. We need to notify the
    # client by sending back the UID of the command.
    #
    # Format:  "UID#end$"
    #
    def onInput_onCommandFinished(self,cmd):
        try:
            self.connectedSocket.sendall(cmd[0]+"#end$")
            
        except Exception as ex:
            self.logger.warn("Unable to send back command complete notification")
            self.logger.warn(ex)
            
        pass

You find the box in its natural habitat in  the NAO4Scratch repo on GitHub. Note that you still find a lot if “self.logger.info” statements in the code. These are present in order to have a complete overview over the behavior as long as the project is in alpha/beta state. They will be replaced by debug output later.

Leave a Reply

Your email address will not be published. Required fields are marked *