home *** CD-ROM | disk | FTP | other *** search
- # ***** BEGIN LICENSE BLOCK *****
- # Version: MPL 1.1/GPL 2.0/LGPL 2.1
- #
- # The contents of this file are subject to the Mozilla Public License Version
- # 1.1 (the "License"); you may not use this file except in compliance with
- # the License. You may obtain a copy of the License at
- # http://www.mozilla.org/MPL/
- #
- # Software distributed under the License is distributed on an "AS IS" basis,
- # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- # for the specific language governing rights and limitations under the
- # License.
- #
- # The Original Code is the Python Computer Graphics Kit.
- #
- # The Initial Developer of the Original Code is Matthias Baas.
- # Portions created by the Initial Developer are Copyright (C) 2004
- # the Initial Developer. All Rights Reserved.
- #
- # Contributor(s):
- #
- # Alternatively, the contents of this file may be used under the terms of
- # either the GNU General Public License Version 2 or later (the "GPL"), or
- # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- # in which case the provisions of the GPL or the LGPL are applicable instead
- # of those above. If you wish to allow use of your version of this file only
- # under the terms of either the GPL or the LGPL, and not to allow others to
- # use your version of this file under the terms of the MPL, indicate your
- # decision by deleting the provisions above and replace them with the notice
- # and other provisions required by the GPL or the LGPL. If you do not delete
- # the provisions above, a recipient may use your version of this file under
- # the terms of any one of the MPL, the GPL or the LGPL.
- #
- # ***** END LICENSE BLOCK *****
- # $Id: tunnel.py,v 1.2 2005/08/08 18:05:34 mbaas Exp $
-
- ## \file tunnel.py
- ## Contains the Tunnel class.
-
- from cgtypes import *
- import component
- from slots import *
- import _core
- import socket, threading, struct
- import scene
-
- # _ImmediateForwarder
- class _ImmediateForwarder(NotificationForwarder):
- """Internal class for the Tunnel component.
-
- This forwarder immediately sends the new value to the server part
- of the tunnel.
- """
- def __init__(self, tunnel, slot, msgid, idx):
- NotificationForwarder.__init__(self, self.sendValue)
- self.tunnel = tunnel
- self.slot = slot
- self.msgid = msgid
- self.idx = idx
-
- def sendValue(self):
- t = self.tunnel
- msg = t.encodeValueMessage(self.msgid, self.idx, self.slot)
- t.send(msg)
-
- # _GatherForwarder
- class _GatherForwarder(NotificationForwarder):
- """Internal class for the Tunnel component.
-
- This forwarder marks the slot as changed and sends a combined
- message if it's the sender slot (the last slot in the tunnel).
- """
- def __init__(self, tunnel, idx, sender):
- NotificationForwarder.__init__(self, self.markSlot)
- self.tunnel = tunnel
- self.idx = idx
- self.sender = sender
-
- def markSlot(self):
- self.tunnel.markAsChanged(self.idx)
- if self.sender:
- self.tunnel.sendBulk()
-
-
-
- # Tunnel
- class Tunnel(component.Component):
- """%Tunnel component which can connect slots on different machines.
-
- A tunnel is a component that has an arbitrary number of input slots
- and a corresponding output slot for each input slot.
- The value of the input slot is simply passed to the output slot.
- The speciality of the tunnel is that both ends may reside on two
- different machines.
-
- \image html tunnel.png
-
- An instance of the tunnel component either represents the input part
- (client) or the output part (server). Value changes on the client are
- then propagated to the server. When creating either side of the tunnel
- you have to define the name and type of the slots that should be created.
- The slot types of the client and the server should always match.
-
- Note: ArraySlots are currently not supported.
-
- Example:
-
- \code
- # On machine A
- t = Tunnel(
- slots = [("pos", "Vec3Slot()"),
- ("rot", "Mat3Slot()")],
- host = "<name or IP of machine B>",
- )
-
- # Do connections between other components and t.pos_slot/t.rot_slot
- # or set the values directly on the tunnel
- \endcode
-
- \code
- # On machine B
- t = Tunnel(
- server = True,
- slots = [("pos", "Vec3Slot()"),
- ("rot", "Mat3Slot()")],
- )
-
- # Make connections between t.pos_slot/t.rot_slot and other components
- # or retrieve the values directly from the tunnel
- \endcode
-
- \par Protocol
-
- The client sends its messages as UDP datagrams to the server.
- One packet may contain several individual messages that are just
- concatenated. The first two bytes of each message is the message
- ID. The remaining part depends on the ID. The byte order is always
- in big endian.
-
- - ID 0: Init message (must be the only message in a packet)
- - ID 1: Binary int value
- - ID 2: Binary double value
- - ID 3: Binary bool value (actually short int)
- - ID 4: Binary vec3 value (3 doubles)
- - ID 5: Binary vec4 value (4 doubles)
- - ID 6: Binary mat3 value (9 doubles)
- - ID 7: Binary mat4 value (16 doubles)
- - ID 8: Binary quat value (4 doubles)
-
- All the value messages are composed of the slot index number (short int),
- followed by the binary value.
- """
-
- def __init__(self,
- name = "Tunnel",
- server = False,
- slots = None,
- port = 64738,
- host = "localhost",
- gather_messages = False,
- verbose = False,
- auto_insert = True):
- """Constructor.
-
- The argument \a slot specifies the slots to create on the tunnel.
- It must be a list of 2-tuples containing two strings. The first
- string is the name of the attribute and the second contains the
- type and initializer of the slot (in Python syntax). The order and
- types of the slots on the client and server should always match.
-
- The arguments \a host and \a gather_messages are only meaningful
- on the client side. If \a gather_messages is True, then value changes
- on a slot are not immediately sent but only when the last slot
- receives a new value. You can use this option if you know that
- all slot values will change at the same time. In this case it's more
- efficient to change all values and then send only one message
- containing all new values instead of sending individual messages for
- every slot.
-
- \param name (\c str) Component name
- \param server (\c bool) True if this is the server part
- \param slots (\c list) A list of slot definitions. Each definition is a tuple (attribute name, slot class and initial arguments).
- \param port (\c int) Port number to use for the data transfer
- \param host (\c str) Host where the tunnel server is running (name or IP address). Client only.
- \param gather_messages (\c bool) True if the messages should be combined. Client only.
- \param verbose (\c bool) True if debug messages should be printed
- \param auto_insert (\c bool) True if the component should be inserted into the scene automatically
- """
- component.Component.__init__(self, name=name, auto_insert=auto_insert)
-
- if slots==None:
- slots = []
-
- self.server = server
- self.host = host
- self.port = port
- self.sock = None
- self.slots = slots
- self.gather_messages = gather_messages
- self.verbose = verbose
-
- if self.server:
- self.host = socket.gethostname()
-
- # A list with tuples (slot, id)
- self.slotobjs = []
- # A dictionary with slot indices as keys. If an index is present
- # in a dictionary then the corresponding slot was modified and the
- # value has to be sent to the server
- self.changed = {}
-
- self.forwarders = []
-
- # Initialize client/server...
- if server:
- self.initServer()
- else:
- self.initClient()
-
- def __str__(self):
- if self.server:
- return 'Tunnel server "%s" listening at %s:%d. %d slots.'%(self.name, self.host, self.port, len(self.slots))
- else:
- return 'Tunnel client "%s". %d slots. Server at %s:%d.'%(self.name, len(self.slots), self.host, self.port)
-
- ## protected:
-
- # initClient
- def initClient(self):
- """Initialization method for the client.
- """
- # Create socket object for sending UDP datagrams
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-
- # Create slot attributes...
- self.createClientSlotAttribs()
-
- # Send init message
- s = map(lambda x: "%s:%s"%x, self.slots)
- s = ";".join(s)
- self.send(struct.pack(">H", 0)+s)
-
- # initServer
- def initServer(self):
- """Initialization method for the server.
- """
- # Create slot attributes...
- self.createServerSlotAttribs()
-
- # Open UDP socket
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- ipaddr = socket.gethostbyname(self.host)
- if self.verbose:
- print "Open UDP port %d on %s (%s)"%(self.port, ipaddr, self.host)
- self.sock.bind((ipaddr, self.port))
-
- # Split thread with the server loop
- self.serverthread = threading.Thread(name="Tunnel-Server",
- target=self.serverLoop)
- self.serverthread.setDaemon(True)
- self.serverthread.start()
-
- # serverLoop
- def serverLoop(self):
- """Server loop.
-
- This method reads messages from the previously created socket
- and process them.
- The method is run in its own thread (because reading from the
- socket is a blocking operation).
- """
-
- if self.verbose:
- print "Tunnel server running at port",self.port
- while 1:
- # Read a packet (blocking)
- rawdata, addr = self.sock.recvfrom(5000)
- if self.verbose:
- print "---Tunnel messages--"
- # Process all messages...
- while rawdata!="":
- # Get the id of the first message...
- try:
- id = struct.unpack(">H", rawdata[:2])[0]
- except:
- print "Error: Invalid UDP packet (no valid message id)."
- rawdata = ""
- continue
-
- # Process the message...
- msgdata = rawdata[2:]
- if id==0:
- if self.verbose:
- print "Init"
- f = msgdata.split(";")
- localslots = map(lambda x: x[1], list(self.iterSlotDefs(self.slots)))
- remoteslots = map(lambda x: tuple(x.split(":")), f)
- remoteslots = map(lambda x: x[1], list(self.iterSlotDefs(remoteslots)))
- mismatch = True
- if len(localslots)==len(remoteslots):
- mismatch = localslots!=remoteslots
- if mismatch:
- print "%s: The types of the remote and local slots don't match."%self.name
- rawdata = ""
- else:
- # Decode the slot index and the value to set...
- try:
- n,idx,v = self.decodeValueMessage(id, msgdata)
- except struct.error, e:
- print e
- rawdata = ""
- continue
- except ValueError, e:
- print e
- rawdata = ""
- continue
- # Prepare the next message...
- rawdata = msgdata[n:]
- if self.verbose:
- print "Message id: %d - Slot:%d Value:%s"%(id,idx,v)
- # Get the slot that will receive the new value...
- try:
- slot,id = self.slotobjs[idx]
- except IndexError,e:
- print "Error: Invalid slot index (%d)"%idx
- # Set the new value...
- try:
- slot.setValue(v)
- except:
- print "Error: Could not assign value",v,"to slot of type",slot.typeName()
-
- self.sock.close()
- if self.verbose:
- print "Tunnel server stopped at port",self.port
-
- # markAsChanged
- def markAsChanged(self, idx):
- """Mark a slot that has changed its value.
- """
- self.changed[idx] = 1
-
- # send
- def send(self, msg):
- """Send one or more messages to the server.
-
- \param msg (\c str) Message(s)
- """
- self.sock.sendto(msg, (self.host, self.port))
-
- # sendBuld
- def sendBulk(self):
- """Send the values of all slots that have changed since the last call.
- """
- msgs = ""
- for idx in self.changed.keys():
- slot,id = self.slotobjs[idx]
- msgs += self.encodeValueMessage(id, idx, slot)
-
- self.send(msgs)
- self.changed = {}
-
- # createClientSlotAttribs
- def createClientSlotAttribs(self):
- slots = self.slots
- if slots==None:
- return
-
- self.forwarders = []
- slotnames = ["<none>", "IntSlot", "DoubleSlot", "BoolSlot", "Vec3Slot",
- "Vec4Slot", "Mat3Slot", "Mat4Slot", "QuatSlot"]
-
- for varname, slotname, slotparams in self.iterSlotDefs(slots):
- # Create the slot object
- exec "slot = %s%s"%(slotname, slotparams)
- # Add the slot as attribute
- setattr(self, "%s_slot"%varname, slot)
- exec "self.addSlot('%s', self.%s_slot)"%(varname, varname)
-
- id = slotnames.index(slotname)
- idx = len(self.forwarders)
-
- self.slotobjs.append((slot,id))
-
- # Create the forwarder object
- if self.gather_messages:
- f = _GatherForwarder(self, idx, idx==len(slots)-1)
- else:
- f = _ImmediateForwarder(self, slot, id, idx)
- slot.addDependent(f)
- self.forwarders.append(f)
-
- # createServerSlotAttribs
- def createServerSlotAttribs(self):
- slots = self.slots
- if slots==None:
- return
-
- slotnames = ["<none>", "IntSlot", "DoubleSlot", "BoolSlot", "Vec3Slot",
- "Vec4Slot", "Mat3Slot", "Mat4Slot", "QuatSlot"]
-
- for varname, slotname, slotparams in self.iterSlotDefs(slots):
- # Create the slot object
- exec "slot = %s%s"%(slotname, slotparams)
- # Add the slot as attribute
- setattr(self, "%s_slot"%varname, slot)
- exec "self.addSlot('%s', self.%s_slot)"%(varname, varname)
-
- id = slotnames.index(slotname)
- idx = len(self.forwarders)
-
- self.slotobjs.append((slot,id))
-
-
- # deleteSlotAttribs
- def deleteSlotAttribs(self):
- if self.slots==None:
- return
-
- for name,slot in self.slots:
- slotname = "%s_slot"%name
- exec "del self.%s"%slotname
-
- self.slots = None
-
- # encodeValueMessage
- def encodeValueMessage(self, id, idx, slot):
- """Encode a binary value message.
-
- \param id (\c int) Message ID
- \param idx (\c int) Slot index
- \param slot (\c Slot) Corresponding slot
- """
-
- # int
- if id==1:
- return struct.pack(">HHi", id, idx, slot.getValue())
- # double
- elif id==2:
- return struct.pack(">HHd", id, idx, slot.getValue())
- # bool
- elif id==3:
- return struct.pack(">HHh", id, idx, slot.getValue())
- # vec3
- elif id==4:
- x,y,z = slot.getValue()
- return struct.pack(">HHddd", id, idx, x, y, z)
- # vec4
- elif id==5:
- x,y,z,w = slot.getValue()
- return struct.pack(">HHdddd", id, idx, x, y, z,w)
- # mat3
- elif id==6:
- M = slot.getValue()
- return struct.pack(">HHddddddddd", id, idx, *M.toList(rowmajor=True))
- # mat4
- elif id==7:
- M = slot.getValue()
- return struct.pack(">HHdddddddddddddddd", id, idx, *M.toList(rowmajor=True))
- # quat
- elif id==8:
- q = slot.getValue()
- return struct.pack(">HHdddd", id, idx, q.w, q.x, q.y, q.z)
-
- # decodeValueMessage
- def decodeValueMessage(self, id, msg):
- """Decode a binary value message.
-
- Raises a ValueError exception if the id is invalid. If the
- size of msg is too short, a struct.error exception is thrown.
-
- \param id (\c int) Message ID
- \param msg (\c str) Data part of the message (may contain additional messages)
- \return Tuple (Message size, slot index, value)
- """
-
- # int
- if id==1:
- id, v = struct.unpack(">Hi", msg[:6])
- return 6,id,v
- # double
- elif id==2:
- id, v = struct.unpack(">Hd", msg[0:10])
- return 10,id,v
- # bool
- elif id==3:
- id, v = struct.unpack(">Hh", msg[:4])
- return 4,id,v
- # vec3
- elif id==4:
- idx, x,y,z = struct.unpack(">Hddd", msg[:26])
- return (26, idx, vec3(x,y,z))
- # vec4
- elif id==5:
- idx, x,y,z,w = struct.unpack(">Hdddd", msg[:34])
- return (34, idx, vec4(x,y,z,w))
- # mat3
- elif id==6:
- idx, m11,m12,m13,m21,m22,m23,m31,m32,m33 = struct.unpack(">Hddddddddd", msg[:74])
- return (74, idx, mat3(m11,m12,m13,m21,m22,m23,m31,m32,m33))
- # mat4
- elif id==7:
- idx, m11,m12,m13,m14,m21,m22,m23,m24,m31,m32,m33,m34,m41,m42,m43,m44 = struct.unpack(">Hdddddddddddddddd", msg[:130])
- return (130, idx, mat4(m11,m12,m13,m14,m21,m22,m23,m24,m31,m32,m33,m34,m41,m42,m43,m44))
- # quat
- elif id==8:
- idx, w,x,y,z = struct.unpack(">Hdddd", msg[:34])
- return (34, idx, quat(w,x,y,z))
- else:
- raise ValueError, "Unknown message id (%d)"%id
-
-
- # iterSlotDefs
- def iterSlotDefs(self, slots):
- """Generates the variable name, slot name and slot parameters.
-
- This generator method takes a slot definition list and generates
- 3-tuples (variable name, slot name, slot parameters).
-
- For example, the slot definition list [("xpos", "IntSlot(2)"),
- ("ypos", "IntSlot(12)")] will generate two tuples ("xpos", "IntSlot",
- "(2)") and ("ypos", "IntSlot", "(12)").
- """
-
- for varname, slotdef in slots:
- n = slotdef.find("(")
- if n!=-1:
- slotname = slotdef[:n]
- slotparams = slotdef[n:]
- else:
- slotname = slotdef
- slotparams = "()"
- yield varname, slotname, slotparams
-
-