Categories: S60 | Python | How To | Code Examples | Networking | TCP/IP
This page was last modified 22:27, 12 October 2007.
How to control a robot from PyS60
From Forum Nokia Wiki
To control a robot, we need a server, a client and a protocol to communicate between both.
In this example, I use some piece of source from a project of mine called myMRC (my Mobile Remote Control). Initially the project uses a mobile client written in Java, but soon the use of Python on the phone was more than great for debugging the server.
We have the following environment:
- Server: Linux Debian machine with Gnome.
- Client: S60 phone with PyS60 installed.
- The robot: we will control here a multimedia player (Totem) but it would be the same server structure to control any kind of robot.
Contents |
Communication protocol
We came out with the following solution to send/receive commands, arguments, data(XML)...
Packet format: ------------- +-------------------------------------------------------+-----------//------------+ | header | Data | +-----------+-------------------------------------------+-----------//------------+ | 0 1 | 2 3 | 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 20 21 22 ... Max length | +-----------+-------------------------------------------+-----------//------------+ | OPCode | Data length | Data | +-----------+-------------------------------------------+-----------//------------+
- OPCode:
The opcode has 4 characters. The 2 first characters define the group i.e: the robot you want to control for example. Then the 2 second characters define the command. With this structure we have up to 100 groups of 100 commands, which let's us big possibilities to control about anything.
- Data length: 16 characters are reserved to the data length. So the structure would fit also for enormous files!
- Data: starting from character 20 to the end of the packet we have the data.
If the header + data exceed the maximum length for the packet, the first one will be sent with the header and all others without since when parsing the header, the client or the server will expect a certain amount of data to be received.
The server
Specification
Configuration files
All relevant configuration data will be stored in configuration files. (mymrc.conf, players.conf)
Concurrent connections
One of the specifications was to have concurrent connection to control the same player or different robot at the same time. A simple case is that if one is using his/hers mobile phone to control the robot but wants to switch to his/hers PDA, their is no need to disconnect the phone. Event better! If your server has the ability to pilot a USB device to control the coffee machine, you can activate it when you leave the office even if someone is changing the music at home... Only your imagination puts limits :)
Cross platform server
The server is written in Python since it is much easier to implement and also it is cross-platfrom.
The server has been under developed on Linux at the moment so it won't work on Windows machines (the controller part). Not just yet. But you can find some source to adapt like PyS60RC
Source code
- mymrc.conf
[inet] #interface=eth0 port=1233 host=11.2.1.4 maxclient=5 [bluetooth] #interface=eth0 # 0 to 9 port=1 host=localhost [database] dbname=mymrcdb [players] audio=totem #video= #cd= #dvd= [settings] printdebug = yes language=en audio=1 video=0 dvd=0 tv=0 # do not modify unless you know what you are doing! [protocol] BufferSize=1024 DataLength=16
- players.conf
[totem] start=totem info=totem gnome player play =--play stop=--pause pause=--play-pause next=--next close=--quit previous=--previous forward=--seek-fwd backward=--seek-bwd volume-up=--volume-up volume-down=--volume-down fullscreen=--fullscreen toogle-controls=--toggle-controls enqueue=--enqueue replace=--replace
- mymrcd.py
## myMRC deamon # @author LEFEVRE Damien # @version 0.1 # @decription myMRC server daemon from ConfigParser import ConfigParser from popen2 import popen3 as popen import thread, threading, socket import os, sys welcomeScreen = """******************************************************************************** Welcome to myMRC by: GREGORI Sven & LEFEVRE Damien http://www.mymrc.org ******************************************************************************** """ debug = True ## Trace debugging messages. # @param aString String to be printed. def printd( aString ): if debug: print aString ## Operation code dictionary. class OPCode( dict ): ## The constructor # @param self: The object pointer. def __init__( self ): dict.__init__(self) self["EV_NULL"] = "0000" self["EV_INFO"] = "0100" self["EV_PLAY"] = "0101" self["EV_STOP"] = "0102" self["EV_PAUSE"] = "0103" self["EV_NEXT"] = "0104" self["EV_CLOSE"] = "0105" self["EV_PREVIOUS"] = "0106" self["EV_FORWARD"] = "0107" self["EV_BACKWARD"] = "0108" self["EV_VOLUMEUP"] = "0109" self["EV_VOLUMEDOWN"] = "0110" self["EV_FULLSCREEN"] = "0111" self["EV_TOOGLECONTROLS"] = "0112" ############################################################################### # Settings classes ############################################################################### ## Settings class. # Used to retrieve settings from the configuration file. class Players( object ): ## Stores the unique Singleton instance- _iInstance = None ## Players class class PlayersClass( ConfigParser ): # The constructor. # @param self: The object pointer def __init__( self, aCwd ): ConfigParser.__init__( self ) try: self.readfp( open( os.path.join( os.getcwd(), "players.conf" ))) except IOError, error: print "players.conf is missing." sys.exit(1) ## The constructor # @param self The object pointer. def __init__( self, aCwd=None ): # Check whether we already have an instance if Players._iInstance is None: # Create and remember instanc Players._iInstance = Players.PlayersClass(aCwd) # Store instance reference as the only member in the handle self.__dict__['_EventHandler_instance'] = Players._iInstance ## Delegate access to implementation. # @param self The object pointer. # @param attr Attribute wanted. # @return Attribute def __getattr__(self, aAttr): return getattr(self._iInstance, aAttr) ## Delegate access to implementation. # @param self The object pointer. # @param attr Attribute wanted. # @param value Vaule to be set. # @return Result of operation. def __setattr__(self, aAttr, aValue): return setattr(self._iInstance, aAttr, aValue ) ## Settings class. # Used to retrieve settings from the configuration file. class Settings( object ): ## Stores the unique Singleton instance- _iInstance = None class SettingsClass( ConfigParser ): # The constructor. # @param self: The object pointer def __init__( self, aCwd ): ConfigParser.__init__( self ) try: self.readfp( open( os.path.join( os.getcwd(), "mymrc.conf" ) )) except IOError, error: print "mymrc.conf is missing" sys.exit(1) if not self.has_section( "database" ): DatabaseSectionError( ) ## The constructor # @param self The object pointer. def __init__( self, aCwd=None ): # Check whether we already have an instance if Settings._iInstance is None: # Create and remember instanc Settings._iInstance = Settings.SettingsClass(aCwd) # Store instance reference as the only member in the handle self.__dict__['_EventHandler_instance'] = Settings._iInstance ## Delegate access to implementation. # @param self The object pointer. # @param attr Attribute wanted. # @return Attribute def __getattr__(self, aAttr): return getattr(self._iInstance, aAttr) ## Delegate access to implementation. # @param self The object pointer. # @param attr Attribute wanted. # @param value Vaule to be set. # @return Result of operation. def __setattr__(self, aAttr, aValue): return setattr(self._iInstance, aAttr, aValue ) ############################################################################### # Protocol ############################################################################### """ Packet format: ------------- +-----------------------------------------------------+-----------//------------+ | header | Data | +---------+-------------------------------------------+-----------//------------+ | 0 1 2 3 | 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 20 21 22 ... Max length | +---------+-------------------------------------------+-----------//------------+ | OPCode | Data length | Data | +---------+-------------------------------------------+-----------//------------+ """ ## Parse and build packets # class Packet( object ): ## Packet size. _iBufferSize = 1024 ## Data length _iDataLength = 16 def __init__(self): self._iBufferSize = int(Settings().get("protocol", "BufferSize")) self._iDataLength = int(Settings().get("protocol", "DataLength")) ## Get the data length with the proper formating (16 characters value). # @param self: The object pointer. # @return: 16 characters formated length. def _formatedLength( self, aData ): length = str( len( aData ) ) formatedLength = "" i = 0 for i in range( self._iDataLength - len( length ) ): formatedLength += "0" formatedLength += length return formatedLength ## Packet builder class. class PacketBuilder( Packet ): ## Build a packet to be sent to the client. # @param self: The obkect pointer # @param aOpCode: Operation code # @param aData: Data to be sent. # @return: Packet to be sent. def build(self, aOpCode, aData): packet = "" packet += str( aOpCode ) packet += self._formatedLength( aData ) packet += str( aData ) return packet ## Packet parser class. class PacketParser(Packet): ## Parse incoming buffer. # @param self: The object pointer. # @param aBuffer: buffer coming from the server # @return: opCode, length and data as strings. def parse(self, aBuffer): opCode = aBuffer[0:4] length = aBuffer[4:20] data = aBuffer[20:] return (opCode, length, data) ############################################################################### # Networking ############################################################################### # Inet socket server class InetServerSocket( socket.socket ): ## The constructor. # @param self: The object pointer. # @param aHost: Host name to connect to. # @param aPort: Port on the host to bind. # @param aMaxClient: Maximum number of concurrent connection. def __init__( self, aHost, aPort, aMaxClient ): socket.socket.__init__( self, socket.AF_INET, socket.SOCK_STREAM ) self.bind ( ( str(aHost), int(aPort) ) ) self.listen ( int(aMaxClient) ) self._iLock = threading.Semaphore() self._iClientStack = [] self._iMaxClient = int(aMaxClient) printd("Inet interface: (%s:%s) - concurrent connection: %s"%(aHost, aPort, aMaxClient)) ## Add instance to the array. # @param self: The object pointer. # @param aChannel: Channel to had the the array. def _addToStack( self, aClient ): self._iLock.acquire( ) self._iClientStack.append( aClient ) self._iLock.release( ) printd( "Client added to stack") ## Remove instance from array # @param self: The object pointer # @param aChannel: Channel to remove from the queue def _removeFromStack( self, aClient ): self._iLock.acquire( ) self._iClientStack.remove( aClient ) self._iLock.release( ) printd( "Channel removed from stack." ) ## Watch for channel incoming requests. # @param self: The object pointer. # @param aChannel: Channel to remove from the queue. def _channelWatcher( self, aChannel ): while 1: data = aChannel.recv ( self._iBufferSize ) if not data: break else: #retVal = self._mCallback( data ) #retVal = EventHandler().event(data) #if retVal: aChannel.send ( retVal ) pass self._removeFromStack( aChannel ) aChannel.close( ) printd("Connection closed: " + str( aChannel ) ) ## Handle incomming connection request. # @param self: The object pointer. def _connectionHandler( self ): printd("Wait for incoming connection") while True: if len(self._iClientStack) in range( self._iMaxClient ): #channel, details = self.accept() #printd( 'New connection with: ' + str(details) ) # new client client = Client(self.accept()) self._addToStack( client ) #self._addToStack( channel ) #channel.setblocking( 1 ) #thread.start_new_thread( self._channelWatcher, ( channel, ) ) else: printd("Maximum concurrent connection exceeded. Close client \ connection before creating a new one") ## Listen interface for incomming connection. # @param self The object pointer. def start( self ): thread.start_new_thread(self._connectionHandler, () ) ## Send data # @param self The object pointer. # @param aFrame Frame. def send( self, aFrame): pass def __del__(self): for client in self._iClientStack: self._removeFromStack(client) del client ## Inet server singletong class InetServer( object ): ## Stores the unique Singleton instance- _iInstance = None ## Inet server class declaration class InetServerClass: ## The constructor. # @param self: The object pointer. def __init__( self ): self._iSocketServer = None ## Start the server. # @param self The object pointer. def start(self): self._iSocketServer = InetServerSocket( Settings().get("inet","host"), Settings().get("inet","port"), Settings().get("inet","maxclient")) self._iSocketServer.start() printd("InetServer started") ## Stop the server. # @param self The object pointer. def stop(self): if self._iSocketServer: del self._iSocketServer self._iSocketServer = None printd("InetServer stopped") ## Restart server. # @param self The object pointer. def restart(self): printd("Restarting InetServer") self.stop() self.start() ## The destructor # @param self: The object pointer def __del__( self ): self.stop() ########################################################################### # Singleton accessors ########################################################################### ## The constructor # @param self The object pointer. def __init__( self ): # Check whether we already have an instance if InetServer._iInstance is None: # Create and remember instanc InetServer._iInstance = InetServer.InetServerClass() # Store instance reference as the only member in the handle self.__dict__['_EventHandler_instance'] = InetServer._iInstance ## Delegate access to implementation. # @param self The object pointer. # @param attr Attribute wanted. # @return Attribute def __getattr__(self, aAttr): return getattr(self._iInstance, aAttr) ## Delegate access to implementation. # @param self The object pointer. # @param attr Attribute wanted. # @param value Vaule to be set. # @return Result of operation. def __setattr__(self, aAttr, aValue): return setattr(self._iInstance, aAttr, aValue ) ############################################################################### # Controlers ############################################################################### class Controller( object ): def __init__(self): if sys.platform == 'win32': self._shell, self._tail = ('cmd', '\r\n') else: self._shell, self._tail = ('sh', '\n') def cmd( self, aCommand ): try: printd(aCommand) popen(aCommand+self._tail) except Exception, error: printd('AudioController: '+ repr(error)) return '0000', 'error' class AudioController( Controller, Players ): def __init__(self): Controller.__init__(self) Players.__init__(self) self._iPlayer = Settings().get("players", "audio") printd("Audio player: " + self._iPlayer) self._iNBSP = " " def play(self, aFile=None): command = self.get(self._iPlayer, "start") + self._iNBSP + \ self.get(self._iPlayer, "play") + self._iNBSP + \ "\"%s\"" % aFile thread.start_new(self.cmd, (command,)) return "Play" def stop(self): command = self.get(self._iPlayer, "start")+ \ self._iNBSP+self.get(self._iPlayer, "stop") return self.cmd(command) def pause(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "pause") return self.cmd(command) def next(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "next") return self.cmd(command) def close(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "close") return self.cmd(command) def previous(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "previous") return self.cmd(command) def forward(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "forward") return self.cmd(command) def backward(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "backward") return self.cmd(command) def volume_up(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "volume-up") return self.cmd(command) ## volume down. # @callgraph # @callergraph def volume_down(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "volume-down") return self.cmd(command) def fullscreen(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "fullscreen") return self.cmd(command) def hide_show_controls(self): command = self.get(self._iPlayer, "start")+\ self._iNBSP+self.get(self._iPlayer, "toogle-controls") return self.cmd(command) def __del__(self): printd("AudioController: Destructor") self.close() ############################################################################### # Client ############################################################################### class Client: def __init__(self, aConnection ): self._iConn = aConnection[0] self._iConn.setblocking( 1 ) self._iObserverPid = 0 self._iBufferSize = int(Settings().get("protocol", "BufferSize")) print self._iBufferSize self._iAudioController = AudioController() self.start_observer() printd( 'New connection with: ' + str(aConnection[1]) ) def _observer(self): # set PID to be able to kill the thread try: self._iObserverPid = os.getpid() except: pass while 1: data = self._iConn.recv( self._iBufferSize ) if not data: break else: retVal = self._handle_event(data) if retVal: self.send(PacketBuilder().build(retVal[0], retVal[1])) self._iConn.close( ) self._iObserverPid = 0 printd("Connection closed: " + str( self._iConn ) ) def send(self, aString): self._iConn.sendall(aString, self._iBufferSize) def receive(self): pass def start_observer(self): print "start_observer(self):" thread.start_new_thread(self._observer, () ) def stop_observer(self): if self._iObserverPid: os.popen("kill -9 "+str(self._iObserverPid)) def _handle_event(self, aData): opcode, length, data = PacketParser().parse(aData) if opcode == OPCode()['EV_INFO']: self._iAudioController.play() return OPCode()['EV_NULL'], "info" elif opcode == OPCode()["EV_PLAY"]: song = "/home/storage/Music/Counting Crows - Mr. Jones.mp3" self._iAudioController.play( song ) return OPCode()['EV_NULL'], "Playing" elif opcode == OPCode()["EV_STOP"]: self._iAudioController.stop() return OPCode()['EV_NULL'], "Stopped" elif opcode == OPCode()["EV_PAUSE"]: self._iAudioController.pause() return OPCode()['EV_NULL'], "Play/Pause" elif opcode == OPCode()["EV_NEXT"]: self._iAudioController.next() return OPCode()["EV_NULL"], "Next" elif opcode == OPCode()["EV_CLOSE"]: self._iAudioController.close() return OPCode()['EV_NULL'], "Closed" elif opcode == OPCode()["EV_PREVIOUS"]: self._iAudioController.previous() return OPCode()['EV_NULL'], "Previous" elif opcode == OPCode()["EV_FORWARD"]: self._iAudioController.forward() return OPCode()['EV_NULL'], "Forward" elif opcode == OPCode()["EV_BACKWARD"]: self._iAudioController.backward() return OPCode()['EV_NULL'], "Backward" elif opcode == OPCode()["EV_VOLUMEUP"]: self._iAudioController.volume_up() return OPCode()['EV_NULL'], "Volume up" elif opcode == OPCode()["EV_VOLUMEDOWN"]: self._iAudioController.volume_down() return OPCode()['EV_NULL'], "Volume down" elif opcode == OPCode()["EV_FULLSCREEN"]: self._iAudioController.fullscreen() return OPCode()['EV_NULL'], "Fullscreen" elif opcode == OPCode()["EV_TOOGLECONTROLS"]: self._iAudioController.hide_show_controls() return OPCode()['EV_NULL'], "Toogle controls" else: return "0000", '' def __del__(self): self._iEngine() self._iConn.close() ############################################################################### # Utils ############################################################################### class App_lock: ## The constructor. # @param self: The object pointer. def __init__( self ): ## class semaphore self._iAppLock = threading.Semaphore() self._iAppLock.acquire() ## Wait forever until the lock is signaled. # @param self: The object pointer. def wait( self ): # Wait forever to acquire the semaphore. self._iAppLock.acquire(True) # Release semaphore; the application exit. self._iAppLock.release() ## Signal the lock to exit the application. # @param self: The object pointer. def signal(self): self._iAppLock.release() ## myMRC daemon server class. class MyMRCd: ## The constructor. # @param self The object pointer. def __init__(self): print welcomeScreen # Set the confile file path Settings( os.getcwd() ) Players( os.getcwd() ) ## Run the server # @param self The object pointer. def run(self): InetServer().start() ## Destructor. Stops the networking servers # @param self The object pointer. def __del__(self): InetServer().stop() if __name__ == '__main__': APP_LOCK = App_lock() myMRC = MyMRCd() myMRC.run() APP_LOCK.wait( )
The PyS60 client
Here come a minimalistic client meant to debug the server.
Make sure to put an exact copy of the opcode dictionary on the client and the server.
With this script below, you will have to set the 'ip' and 'port' to connect to on the server as well as a valid video or audio file in 'play'
Source code
- MyMRCpy.py
## myMRC deamon # @author LEFEVRE Damien # @version 0.1 # @decription myMRC server daemon from ConfigParser import ConfigParser from popen2 import popen3 as popen import thread, threading, socket import os, sys welcomeScreen = """******************************************************************************** Welcome to myMRC by: GREGORI Sven & LEFEVRE Damien http://www.mymrc.org ******************************************************************************** """ debug = True ## Trace debugging messages. # @param aString String to be printed. def printd( aString ): if debug: print aString ## Operation code dictionary. class OPCode( dict ): ## The constructor # @param self: The object pointer. def __init__( self ): dict.__init__(self) self["EV_NULL"] = "0000" self["EV_INFO"] = "0100" self["EV_PLAY"] = "0101" self["EV_STOP"] = "0102" self["EV_PAUSE"] = "0103" self["EV_NEXT"] = "0104" self["EV_CLOSE"] = "0105" self["EV_PREVIOUS"] = "0106" self["EV_FORWARD"] = "0107" self["EV_BACKWARD"] = "0108" self["EV_VOLUMEUP"] = "0109" self["EV_VOLUMEDOWN"] = "0110" self["EV_FULLSCREEN"] = "0111" self["EV_TOOGLECONTROLS"] = "0112" ############################################################################### # Settings classes ############################################################################### ## Settings class. # Used to retrieve settings from the configuration file. class Players( object ): ## Stores the unique Singleton instance- _iInstance = None ## Players class class PlayersClass( ConfigParser ): # The constructor. # @param self: The object pointer def __init__( self, aCwd ): ConfigParser.__init__( self )
