tim@3: #!/usr/bin/python tim@3: """ tim@3: This module contains an OpenSoundControl implementation (in Pure Python), based (somewhat) on the tim@3: good old 'SimpleOSC' implementation by Daniel Holth & Clinton McChesney. tim@3: tim@3: This implementation is intended to still be 'Simple' to the user, but much more complete tim@3: (with OSCServer & OSCClient classes) and much more powerful tim@3: (the OSCMultiClient supports subscriptions & message-filtering, tim@3: OSCMessage & OSCBundle are now proper container-types) tim@3: tim@3: ================ tim@3: OpenSoundControl tim@3: ================ tim@3: tim@3: OpenSoundControl is a network-protocol for sending (small) packets of addressed data over network sockets. tim@3: This OSC-implementation uses the UDP/IP protocol for sending and receiving packets. tim@3: (Although it is theoretically possible to send OSC-packets over TCP, almost all known implementations use UDP) tim@3: tim@3: OSC-packets come in two kinds: tim@3: - OSC-messages consist of an 'address'-string (not to be confused with a (host:port) network-address!), tim@3: followed by a string of 'typetags' associated with the message's arguments (ie. 'payload'), tim@3: and finally the arguments themselves, encoded in an OSC-specific way. tim@3: The OSCMessage class makes it easy to create & manipulate OSC-messages of this kind in a 'pythonesque' way tim@3: (that is, OSCMessage-objects behave a lot like lists) tim@3: tim@3: - OSC-bundles are a special type of OSC-message containing only OSC-messages as 'payload'. Recursively. tim@3: (meaning; an OSC-bundle could contain other OSC-bundles, containing OSC-bundles etc.) tim@3: OSC-bundles start with the special keyword '#bundle' and do not have an OSC-address. (but the OSC-messages tim@3: a bundle contains will have OSC-addresses!) tim@3: Also, an OSC-bundle can have a timetag, essentially telling the receiving Server to 'hold' the bundle until tim@3: the specified time. tim@3: The OSCBundle class allows easy cration & manipulation of OSC-bundles. tim@3: tim@3: see also http://opensoundcontrol.org/spec-1_0 tim@3: tim@3: --------- tim@3: tim@3: To send OSC-messages, you need an OSCClient, and to receive OSC-messages you need an OSCServer. tim@3: tim@3: The OSCClient uses an 'AF_INET / SOCK_DGRAM' type socket (see the 'socket' module) to send tim@3: binary representations of OSC-messages to a remote host:port address. tim@3: tim@3: The OSCServer listens on an 'AF_INET / SOCK_DGRAM' type socket bound to a local port, and handles tim@3: incoming requests. Either one-after-the-other (OSCServer) or in a multi-threaded / multi-process fashion tim@3: (ThreadingOSCServer / ForkingOSCServer). If the Server has a callback-function (a.k.a. handler) registered tim@3: to 'deal with' (i.e. handle) the received message's OSC-address, that function is called, passing it the (decoded) message tim@3: tim@3: The different OSCServers implemented here all support the (recursive) un-bundling of OSC-bundles, tim@3: and OSC-bundle timetags. tim@3: tim@3: In fact, this implementation supports: tim@3: tim@3: - OSC-messages with 'i' (int32), 'f' (float32), 's' (string) and 'b' (blob / binary data) types tim@3: - OSC-bundles, including timetag-support tim@3: - OSC-address patterns including '*', '?', '{,}' and '[]' wildcards. tim@3: tim@3: (please *do* read the OSC-spec! http://opensoundcontrol.org/spec-1_0 it explains what these things mean.) tim@3: tim@3: In addition, the OSCMultiClient supports: tim@3: - Sending a specific OSC-message to multiple remote servers tim@3: - Remote server subscription / unsubscription (through OSC-messages, of course) tim@3: - Message-address filtering. tim@3: tim@3: --------- tim@3: tim@3: Stock, V2_Lab, Rotterdam, 2008 tim@3: tim@3: ---------- tim@3: Changelog: tim@3: ---------- tim@3: v0.3.0 - 27 Dec. 2007 tim@3: Started out to extend the 'SimpleOSC' implementation (v0.2.3) by Daniel Holth & Clinton McChesney. tim@3: Rewrote OSCMessage tim@3: Added OSCBundle tim@3: tim@3: v0.3.1 - 3 Jan. 2008 tim@3: Added OSClient tim@3: Added OSCRequestHandler, loosely based on the original CallbackManager tim@3: Added OSCServer tim@3: Removed original CallbackManager tim@3: Adapted testing-script (the 'if __name__ == "__main__":' block at the end) to use new Server & Client tim@3: tim@3: v0.3.2 - 5 Jan. 2008 tim@3: Added 'container-type emulation' methods (getitem(), setitem(), __iter__() & friends) to OSCMessage tim@3: Added ThreadingOSCServer & ForkingOSCServer tim@3: - 6 Jan. 2008 tim@3: Added OSCMultiClient tim@3: Added command-line options to testing-script (try 'python OSC.py --help') tim@3: tim@3: v0.3.3 - 9 Jan. 2008 tim@3: Added OSC-timetag support to OSCBundle & OSCRequestHandler tim@3: Added ThreadingOSCRequestHandler tim@3: tim@3: v0.3.4 - 13 Jan. 2008 tim@3: Added message-filtering to OSCMultiClient tim@3: Added subscription-handler to OSCServer tim@3: Added support fon numpy/scipy int & float types. (these get converted to 'standard' 32-bit OSC ints / floats!) tim@3: Cleaned-up and added more Docstrings tim@3: tim@3: v0.3.5 - 14 aug. 2008 tim@3: Added OSCServer.reportErr(...) method tim@3: tim@3: ----------------- tim@3: Original Comments tim@3: ----------------- tim@3: tim@3: > Open SoundControl for Python tim@3: > Copyright (C) 2002 Daniel Holth, Clinton McChesney tim@3: > tim@3: > This library is free software; you can redistribute it and/or modify it under tim@3: > the terms of the GNU Lesser General Public License as published by the Free tim@3: > Software Foundation; either version 2.1 of the License, or (at your option) any tim@3: > later version. tim@3: > tim@3: > This library is distributed in the hope that it will be useful, but WITHOUT ANY tim@3: > WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A tim@3: > PARTICULAR PURPOSE. See the GNU Lesser General Public License for more tim@3: > details. tim@3: tim@3: > You should have received a copy of the GNU Lesser General Public License along tim@3: > with this library; if not, write to the Free Software Foundation, Inc., 59 tim@3: > Temple Place, Suite 330, Boston, MA 02111-1307 USA tim@3: tim@3: > For questions regarding this module contact Daniel Holth tim@3: > or visit http://www.stetson.edu/~ProctoLogic/ tim@3: tim@3: > Changelog: tim@3: > 15 Nov. 2001: tim@3: > Removed dependency on Python 2.0 features. tim@3: > - dwh tim@3: > 13 Feb. 2002: tim@3: > Added a generic callback handler. tim@3: > - dwh tim@3: """ tim@3: tim@3: import math, re, socket, select, string, struct, sys, threading, time, types tim@3: from SocketServer import UDPServer, DatagramRequestHandler, ForkingMixIn, ThreadingMixIn tim@3: tim@3: global version tim@3: version = ("0.3","5b", "$Rev: 5294 $"[6:-2]) tim@3: tim@3: global FloatTypes tim@3: FloatTypes = [types.FloatType] tim@3: tim@3: global IntTypes tim@3: IntTypes = [types.IntType] tim@3: tim@3: ## tim@3: # numpy/scipy support: tim@3: ## tim@3: tim@3: try: tim@3: from numpy import typeDict tim@3: tim@3: for ftype in ['float32', 'float64', 'float128']: tim@3: try: tim@3: FloatTypes.append(typeDict[ftype]) tim@3: except KeyError: tim@3: pass tim@3: tim@3: for itype in ['int8', 'int16', 'int32', 'int64']: tim@3: try: tim@3: IntTypes.append(typeDict[itype]) tim@3: IntTypes.append(typeDict['u' + itype]) tim@3: except KeyError: tim@3: pass tim@3: tim@3: # thanks for those... tim@3: del typeDict, ftype, itype tim@3: tim@3: except ImportError: tim@3: pass tim@3: tim@3: ###### tim@3: # tim@3: # OSCMessage classes tim@3: # tim@3: ###### tim@3: tim@3: class OSCMessage(object): tim@3: """ Builds typetagged OSC messages. tim@3: tim@3: OSCMessage objects are container objects for building OSC-messages. tim@3: On the 'front' end, they behave much like list-objects, and on the 'back' end tim@3: they generate a binary representation of the message, which can be sent over a network socket. tim@3: OSC-messages consist of an 'address'-string (not to be confused with a (host, port) IP-address!), tim@3: followed by a string of 'typetags' associated with the message's arguments (ie. 'payload'), tim@3: and finally the arguments themselves, encoded in an OSC-specific way. tim@3: tim@3: On the Python end, OSCMessage are lists of arguments, prepended by the message's address. tim@3: The message contents can be manipulated much like a list: tim@3: >>> msg = OSCMessage("/my/osc/address") tim@3: >>> msg.append('something') tim@3: >>> msg.insert(0, 'something else') tim@3: >>> msg[1] = 'entirely' tim@3: >>> msg.extend([1,2,3.]) tim@3: >>> msg += [4, 5, 6.] tim@3: >>> del msg[3:6] tim@3: >>> msg.pop(-2) tim@3: 5 tim@3: >>> print msg tim@3: /my/osc/address ['something else', 'entirely', 1, 6.0] tim@3: tim@3: OSCMessages can be concatenated with the + operator. In this case, the resulting OSCMessage tim@3: inherits its address from the left-hand operand. The right-hand operand's address is ignored. tim@3: To construct an 'OSC-bundle' from multiple OSCMessage, see OSCBundle! tim@3: tim@3: Additional methods exist for retreiving typetags or manipulating items as (typetag, value) tuples. tim@3: """ tim@3: def __init__(self, address=""): tim@3: """Instantiate a new OSCMessage. tim@3: The OSC-address can be specified with the 'address' argument tim@3: """ tim@3: self.clear(address) tim@3: tim@3: def setAddress(self, address): tim@3: """Set or change the OSC-address tim@3: """ tim@3: self.address = address tim@3: tim@3: def clear(self, address=""): tim@3: """Clear (or set a new) OSC-address and clear any arguments appended so far tim@3: """ tim@3: self.address = address tim@3: self.clearData() tim@3: tim@3: def clearData(self): tim@3: """Clear any arguments appended so far tim@3: """ tim@3: self.typetags = "," tim@3: self.message = "" tim@3: tim@3: def append(self, argument, typehint=None): tim@3: """Appends data to the message, updating the typetags based on tim@3: the argument's type. If the argument is a blob (counted tim@3: string) pass in 'b' as typehint. tim@3: 'argument' may also be a list or tuple, in which case its elements tim@3: will get appended one-by-one, all using the provided typehint tim@3: """ tim@3: if type(argument) == types.DictType: tim@3: argument = argument.items() tim@3: elif isinstance(argument, OSCMessage): tim@3: raise TypeError("Can only append 'OSCMessage' to 'OSCBundle'") tim@3: tim@3: if hasattr(argument, '__iter__'): tim@3: for arg in argument: tim@3: self.append(arg, typehint) tim@3: tim@3: return tim@3: tim@3: if typehint == 'b': tim@3: binary = OSCBlob(argument) tim@3: tag = 'b' tim@3: elif typehint == 't': tim@3: binary = OSCTimeTag(argument) tim@3: tag = 't' tim@3: else: tim@3: tag, binary = OSCArgument(argument, typehint) tim@3: tim@3: self.typetags += tag tim@3: self.message += binary tim@3: tim@3: def getBinary(self): tim@3: """Returns the binary representation of the message tim@3: """ tim@3: binary = OSCString(self.address) tim@3: binary += OSCString(self.typetags) tim@3: binary += self.message tim@3: tim@3: return binary tim@3: tim@3: def __repr__(self): tim@3: """Returns a string containing the decode Message tim@3: """ tim@3: return str(decodeOSC(self.getBinary())) tim@3: tim@3: def __str__(self): tim@3: """Returns the Message's address and contents as a string. tim@3: """ tim@3: return "%s %s" % (self.address, str(self.values())) tim@3: tim@3: def __len__(self): tim@3: """Returns the number of arguments appended so far tim@3: """ tim@3: return (len(self.typetags) - 1) tim@3: tim@3: def __eq__(self, other): tim@3: """Return True if two OSCMessages have the same address & content tim@3: """ tim@3: if not isinstance(other, self.__class__): tim@3: return False tim@3: tim@3: return (self.address == other.address) and (self.typetags == other.typetags) and (self.message == other.message) tim@3: tim@3: def __ne__(self, other): tim@3: """Return (not self.__eq__(other)) tim@3: """ tim@3: return not self.__eq__(other) tim@3: tim@3: def __add__(self, values): tim@3: """Returns a copy of self, with the contents of 'values' appended tim@3: (see the 'extend()' method, below) tim@3: """ tim@3: msg = self.copy() tim@3: msg.extend(values) tim@3: return msg tim@3: tim@3: def __iadd__(self, values): tim@3: """Appends the contents of 'values' tim@3: (equivalent to 'extend()', below) tim@3: Returns self tim@3: """ tim@3: self.extend(values) tim@3: return self tim@3: tim@3: def __radd__(self, values): tim@3: """Appends the contents of this OSCMessage to 'values' tim@3: Returns the extended 'values' (list or tuple) tim@3: """ tim@3: out = list(values) tim@3: out.extend(self.values()) tim@3: tim@3: if type(values) == types.TupleType: tim@3: return tuple(out) tim@3: tim@3: return out tim@3: tim@3: def _reencode(self, items): tim@3: """Erase & rebuild the OSCMessage contents from the given tim@3: list of (typehint, value) tuples""" tim@3: self.clearData() tim@3: for item in items: tim@3: self.append(item[1], item[0]) tim@3: tim@3: def values(self): tim@3: """Returns a list of the arguments appended so far tim@3: """ tim@3: return decodeOSC(self.getBinary())[2:] tim@3: tim@3: def tags(self): tim@3: """Returns a list of typetags of the appended arguments tim@3: """ tim@3: return list(self.typetags.lstrip(',')) tim@3: tim@3: def items(self): tim@3: """Returns a list of (typetag, value) tuples for tim@3: the arguments appended so far tim@3: """ tim@3: out = [] tim@3: values = self.values() tim@3: typetags = self.tags() tim@3: for i in range(len(values)): tim@3: out.append((typetags[i], values[i])) tim@3: tim@3: return out tim@3: tim@3: def __contains__(self, val): tim@3: """Test if the given value appears in the OSCMessage's arguments tim@3: """ tim@3: return (val in self.values()) tim@3: tim@3: def __getitem__(self, i): tim@3: """Returns the indicated argument (or slice) tim@3: """ tim@3: return self.values()[i] tim@3: tim@3: def __delitem__(self, i): tim@3: """Removes the indicated argument (or slice) tim@3: """ tim@3: items = self.items() tim@3: del items[i] tim@3: tim@3: self._reencode(items) tim@3: tim@3: def _buildItemList(self, values, typehint=None): tim@3: if isinstance(values, OSCMessage): tim@3: items = values.items() tim@3: elif type(values) == types.ListType: tim@3: items = [] tim@3: for val in values: tim@3: if type(val) == types.TupleType: tim@3: items.append(val[:2]) tim@3: else: tim@3: items.append((typehint, val)) tim@3: elif type(values) == types.TupleType: tim@3: items = [values[:2]] tim@3: else: tim@3: items = [(typehint, values)] tim@3: tim@3: return items tim@3: tim@3: def __setitem__(self, i, val): tim@3: """Set indicatated argument (or slice) to a new value. tim@3: 'val' can be a single int/float/string, or a (typehint, value) tuple. tim@3: Or, if 'i' is a slice, a list of these or another OSCMessage. tim@3: """ tim@3: items = self.items() tim@3: tim@3: new_items = self._buildItemList(val) tim@3: tim@3: if type(i) != types.SliceType: tim@3: if len(new_items) != 1: tim@3: raise TypeError("single-item assignment expects a single value or a (typetag, value) tuple") tim@3: tim@3: new_items = new_items[0] tim@3: tim@3: # finally... tim@3: items[i] = new_items tim@3: tim@3: self._reencode(items) tim@3: tim@3: def setItem(self, i, val, typehint=None): tim@3: """Set indicated argument to a new value (with typehint) tim@3: """ tim@3: items = self.items() tim@3: tim@3: items[i] = (typehint, val) tim@3: tim@3: self._reencode(items) tim@3: tim@3: def copy(self): tim@3: """Returns a deep copy of this OSCMessage tim@3: """ tim@3: msg = self.__class__(self.address) tim@3: msg.typetags = self.typetags tim@3: msg.message = self.message tim@3: return msg tim@3: tim@3: def count(self, val): tim@3: """Returns the number of times the given value occurs in the OSCMessage's arguments tim@3: """ tim@3: return self.values().count(val) tim@3: tim@3: def index(self, val): tim@3: """Returns the index of the first occurence of the given value in the OSCMessage's arguments. tim@3: Raises ValueError if val isn't found tim@3: """ tim@3: return self.values().index(val) tim@3: tim@3: def extend(self, values): tim@3: """Append the contents of 'values' to this OSCMessage. tim@3: 'values' can be another OSCMessage, or a list/tuple of ints/floats/strings tim@3: """ tim@3: items = self.items() + self._buildItemList(values) tim@3: tim@3: self._reencode(items) tim@3: tim@3: def insert(self, i, val, typehint = None): tim@3: """Insert given value (with optional typehint) into the OSCMessage tim@3: at the given index. tim@3: """ tim@3: items = self.items() tim@3: tim@3: for item in reversed(self._buildItemList(val)): tim@3: items.insert(i, item) tim@3: tim@3: self._reencode(items) tim@3: tim@3: def popitem(self, i): tim@3: """Delete the indicated argument from the OSCMessage, and return it tim@3: as a (typetag, value) tuple. tim@3: """ tim@3: items = self.items() tim@3: tim@3: item = items.pop(i) tim@3: tim@3: self._reencode(items) tim@3: tim@3: return item tim@3: tim@3: def pop(self, i): tim@3: """Delete the indicated argument from the OSCMessage, and return it. tim@3: """ tim@3: return self.popitem(i)[1] tim@3: tim@3: def reverse(self): tim@3: """Reverses the arguments of the OSCMessage (in place) tim@3: """ tim@3: items = self.items() tim@3: tim@3: items.reverse() tim@3: tim@3: self._reencode(items) tim@3: tim@3: def remove(self, val): tim@3: """Removes the first argument with the given value from the OSCMessage. tim@3: Raises ValueError if val isn't found. tim@3: """ tim@3: items = self.items() tim@3: tim@3: # this is not very efficient... tim@3: i = 0 tim@3: for (t, v) in items: tim@3: if (v == val): tim@3: break tim@3: i += 1 tim@3: else: tim@3: raise ValueError("'%s' not in OSCMessage" % str(m)) tim@3: # but more efficient than first calling self.values().index(val), tim@3: # then calling self.items(), which would in turn call self.values() again... tim@3: tim@3: del items[i] tim@3: tim@3: self._reencode(items) tim@3: tim@3: def __iter__(self): tim@3: """Returns an iterator of the OSCMessage's arguments tim@3: """ tim@3: return iter(self.values()) tim@3: tim@3: def __reversed__(self): tim@3: """Returns a reverse iterator of the OSCMessage's arguments tim@3: """ tim@3: return reversed(self.values()) tim@3: tim@3: def itervalues(self): tim@3: """Returns an iterator of the OSCMessage's arguments tim@3: """ tim@3: return iter(self.values()) tim@3: tim@3: def iteritems(self): tim@3: """Returns an iterator of the OSCMessage's arguments as tim@3: (typetag, value) tuples tim@3: """ tim@3: return iter(self.items()) tim@3: tim@3: def itertags(self): tim@3: """Returns an iterator of the OSCMessage's arguments' typetags tim@3: """ tim@3: return iter(self.tags()) tim@3: tim@3: class OSCBundle(OSCMessage): tim@3: """Builds a 'bundle' of OSC messages. tim@3: tim@3: OSCBundle objects are container objects for building OSC-bundles of OSC-messages. tim@3: An OSC-bundle is a special kind of OSC-message which contains a list of OSC-messages tim@3: (And yes, OSC-bundles may contain other OSC-bundles...) tim@3: tim@3: OSCBundle objects behave much the same as OSCMessage objects, with these exceptions: tim@3: - if an item or items to be appended or inserted are not OSCMessage objects, tim@3: OSCMessage objectss are created to encapsulate the item(s) tim@3: - an OSC-bundle does not have an address of its own, only the contained OSC-messages do. tim@3: The OSCBundle's 'address' is inherited by any OSCMessage the OSCBundle object creates. tim@3: - OSC-bundles have a timetag to tell the receiver when the bundle should be processed. tim@3: The default timetag value (0) means 'immediately' tim@3: """ tim@3: def __init__(self, address="", time=0): tim@3: """Instantiate a new OSCBundle. tim@3: The default OSC-address for newly created OSCMessages tim@3: can be specified with the 'address' argument tim@3: The bundle's timetag can be set with the 'time' argument tim@3: """ tim@3: super(OSCBundle, self).__init__(address) tim@3: self.timetag = time tim@3: tim@3: def __str__(self): tim@3: """Returns the Bundle's contents (and timetag, if nonzero) as a string. tim@3: """ tim@3: if (self.timetag > 0.): tim@3: out = "#bundle (%s) [" % self.getTimeTagStr() tim@3: else: tim@3: out = "#bundle [" tim@3: tim@3: if self.__len__(): tim@3: for val in self.values(): tim@3: out += "%s, " % str(val) tim@3: out = out[:-2] # strip trailing space and comma tim@3: tim@3: return out + "]" tim@3: tim@3: def setTimeTag(self, time): tim@3: """Set or change the OSCBundle's TimeTag tim@3: In 'Python Time', that's floating seconds since the Epoch tim@3: """ tim@3: if time >= 0: tim@3: self.timetag = time tim@3: tim@3: def getTimeTagStr(self): tim@3: """Return the TimeTag as a human-readable string tim@3: """ tim@3: fract, secs = math.modf(self.timetag) tim@3: out = time.ctime(secs)[11:19] tim@3: out += ("%.3f" % fract)[1:] tim@3: tim@3: return out tim@3: tim@3: def append(self, argument, typehint = None): tim@3: """Appends data to the bundle, creating an OSCMessage to encapsulate tim@3: the provided argument unless this is already an OSCMessage. tim@3: Any newly created OSCMessage inherits the OSCBundle's address at the time of creation. tim@3: If 'argument' is an iterable, its elements will be encapsuated by a single OSCMessage. tim@3: Finally, 'argument' can be (or contain) a dict, which will be 'converted' to an OSCMessage; tim@3: - if 'addr' appears in the dict, its value overrides the OSCBundle's address tim@3: - if 'args' appears in the dict, its value(s) become the OSCMessage's arguments tim@3: """ tim@3: if isinstance(argument, OSCMessage): tim@3: binary = OSCBlob(argument.getBinary()) tim@3: else: tim@3: msg = OSCMessage(self.address) tim@3: if type(argument) == types.DictType: tim@3: if 'addr' in argument: tim@3: msg.setAddress(argument['addr']) tim@3: if 'args' in argument: tim@3: msg.append(argument['args'], typehint) tim@3: else: tim@3: msg.append(argument, typehint) tim@3: tim@3: binary = OSCBlob(msg.getBinary()) tim@3: tim@3: self.message += binary tim@3: self.typetags += 'b' tim@3: tim@3: def getBinary(self): tim@3: """Returns the binary representation of the message tim@3: """ tim@3: binary = OSCString("#bundle") tim@3: binary += OSCTimeTag(self.timetag) tim@3: binary += self.message tim@3: tim@3: return binary tim@3: tim@3: def _reencapsulate(self, decoded): tim@3: if decoded[0] == "#bundle": tim@3: msg = OSCBundle() tim@3: msg.setTimeTag(decoded[1]) tim@3: for submsg in decoded[2:]: tim@3: msg.append(self._reencapsulate(submsg)) tim@3: tim@3: else: tim@3: msg = OSCMessage(decoded[0]) tim@3: tags = decoded[1].lstrip(',') tim@3: for i in range(len(tags)): tim@3: msg.append(decoded[2+i], tags[i]) tim@3: tim@3: return msg tim@3: tim@3: def values(self): tim@3: """Returns a list of the OSCMessages appended so far tim@3: """ tim@3: out = [] tim@3: for decoded in decodeOSC(self.getBinary())[2:]: tim@3: out.append(self._reencapsulate(decoded)) tim@3: tim@3: return out tim@3: tim@3: def __eq__(self, other): tim@3: """Return True if two OSCBundles have the same timetag & content tim@3: """ tim@3: if not isinstance(other, self.__class__): tim@3: return False tim@3: tim@3: return (self.timetag == other.timetag) and (self.typetags == other.typetags) and (self.message == other.message) tim@3: tim@3: def copy(self): tim@3: """Returns a deep copy of this OSCBundle tim@3: """ tim@3: copy = super(OSCBundle, self).copy() tim@3: copy.timetag = self.timetag tim@3: return copy tim@3: tim@3: ###### tim@3: # tim@3: # OSCMessage encoding functions tim@3: # tim@3: ###### tim@3: tim@3: def OSCString(next): tim@3: """Convert a string into a zero-padded OSC String. tim@3: The length of the resulting string is always a multiple of 4 bytes. tim@3: The string ends with 1 to 4 zero-bytes ('\x00') tim@3: """ tim@3: tim@3: OSCstringLength = math.ceil((len(next)+1) / 4.0) * 4 tim@3: return struct.pack(">%ds" % (OSCstringLength), str(next)) tim@3: tim@3: def OSCBlob(next): tim@3: """Convert a string into an OSC Blob. tim@3: An OSC-Blob is a binary encoded block of data, prepended by a 'size' (int32). tim@3: The size is always a mutiple of 4 bytes. tim@3: The blob ends with 0 to 3 zero-bytes ('\x00') tim@3: """ tim@3: tim@3: if type(next) in types.StringTypes: tim@3: OSCblobLength = math.ceil((len(next)) / 4.0) * 4 tim@3: binary = struct.pack(">i%ds" % (OSCblobLength), OSCblobLength, next) tim@3: else: tim@3: binary = "" tim@3: tim@3: return binary tim@3: tim@3: def OSCArgument(next, typehint=None): tim@3: """ Convert some Python types to their tim@3: OSC binary representations, returning a tim@3: (typetag, data) tuple. tim@3: """ tim@3: if not typehint: tim@3: if type(next) in FloatTypes: tim@3: binary = struct.pack(">f", float(next)) tim@3: tag = 'f' tim@3: elif type(next) in IntTypes: tim@3: binary = struct.pack(">i", int(next)) tim@3: tag = 'i' tim@3: else: tim@3: binary = OSCString(next) tim@3: tag = 's' tim@3: tim@3: elif typehint == 'f': tim@3: try: tim@3: binary = struct.pack(">f", float(next)) tim@3: tag = 'f' tim@3: except ValueError: tim@3: binary = OSCString(next) tim@3: tag = 's' tim@3: elif typehint == 'i': tim@3: try: tim@3: binary = struct.pack(">i", int(next)) tim@3: tag = 'i' tim@3: except ValueError: tim@3: binary = OSCString(next) tim@3: tag = 's' tim@3: else: tim@3: binary = OSCString(next) tim@3: tag = 's' tim@3: tim@3: return (tag, binary) tim@3: tim@3: def OSCTimeTag(time): tim@3: """Convert a time in floating seconds to its tim@3: OSC binary representation tim@3: """ tim@3: if time > 0: tim@3: fract, secs = math.modf(time) tim@3: binary = struct.pack('>ll', long(secs), long(fract * 1e9)) tim@3: else: tim@3: binary = struct.pack('>ll', 0L, 1L) tim@3: tim@3: return binary tim@3: tim@3: ###### tim@3: # tim@3: # OSCMessage decoding functions tim@3: # tim@3: ###### tim@3: tim@3: def _readString(data): tim@3: """Reads the next (null-terminated) block of data tim@3: """ tim@3: length = string.find(data,"\0") tim@3: nextData = int(math.ceil((length+1) / 4.0) * 4) tim@3: return (data[0:length], data[nextData:]) tim@3: tim@3: def _readBlob(data): tim@3: """Reads the next (numbered) block of data tim@3: """ tim@3: tim@3: length = struct.unpack(">i", data[0:4])[0] tim@3: nextData = int(math.ceil((length) / 4.0) * 4) + 4 tim@3: return (data[4:length+4], data[nextData:]) tim@3: tim@3: def _readInt(data): tim@3: """Tries to interpret the next 4 bytes of the data tim@3: as a 32-bit integer. """ tim@3: tim@3: if(len(data)<4): tim@3: print "Error: too few bytes for int", data, len(data) tim@3: rest = data tim@3: integer = 0 tim@3: else: tim@3: integer = struct.unpack(">i", data[0:4])[0] tim@3: rest = data[4:] tim@3: tim@3: return (integer, rest) tim@3: tim@3: def _readLong(data): tim@3: """Tries to interpret the next 8 bytes of the data tim@3: as a 64-bit signed integer. tim@3: """ tim@3: tim@3: high, low = struct.unpack(">ll", data[0:8]) tim@3: big = (long(high) << 32) + low tim@3: rest = data[8:] tim@3: return (big, rest) tim@3: tim@3: def _readTimeTag(data): tim@3: """Tries to interpret the next 8 bytes of the data tim@3: as a TimeTag. tim@3: """ tim@3: high, low = struct.unpack(">ll", data[0:8]) tim@3: if (high == 0) and (low <= 1): tim@3: time = 0.0 tim@3: else: tim@3: time = int(high) + float(low / 1e9) tim@3: rest = data[8:] tim@3: return (time, rest) tim@3: tim@3: def _readFloat(data): tim@3: """Tries to interpret the next 4 bytes of the data tim@3: as a 32-bit float. tim@3: """ tim@3: tim@3: if(len(data)<4): tim@3: print "Error: too few bytes for float", data, len(data) tim@3: rest = data tim@3: float = 0 tim@3: else: tim@3: float = struct.unpack(">f", data[0:4])[0] tim@3: rest = data[4:] tim@3: tim@3: return (float, rest) tim@3: tim@3: def decodeOSC(data): tim@3: """Converts a binary OSC message to a Python list. tim@3: """ tim@3: table = {"i":_readInt, "f":_readFloat, "s":_readString, "b":_readBlob} tim@3: decoded = [] tim@3: address, rest = _readString(data) tim@3: if address.startswith(","): tim@3: typetags = address tim@3: address = "" tim@3: else: tim@3: typetags = "" tim@3: tim@3: if address == "#bundle": tim@3: time, rest = _readTimeTag(rest) tim@3: decoded.append(address) tim@3: decoded.append(time) tim@3: while len(rest)>0: tim@3: length, rest = _readInt(rest) tim@3: decoded.append(decodeOSC(rest[:length])) tim@3: rest = rest[length:] tim@3: tim@3: elif len(rest)>0: tim@3: if not len(typetags): tim@3: typetags, rest = _readString(rest) tim@3: decoded.append(address) tim@3: decoded.append(typetags) tim@3: if typetags.startswith(","): tim@3: for tag in typetags[1:]: tim@3: value, rest = table[tag](rest) tim@3: decoded.append(value) tim@3: else: tim@3: raise OSCError("OSCMessage's typetag-string lacks the magic ','") tim@3: tim@3: return decoded tim@3: tim@3: ###### tim@3: # tim@3: # Utility functions tim@3: # tim@3: ###### tim@3: tim@3: def hexDump(bytes): tim@3: """ Useful utility; prints the string in hexadecimal. tim@3: """ tim@3: print "byte 0 1 2 3 4 5 6 7 8 9 A B C D E F" tim@3: tim@3: num = len(bytes) tim@3: for i in range(num): tim@3: if (i) % 16 == 0: tim@3: line = "%02X0 : " % (i/16) tim@3: line += "%02X " % ord(bytes[i]) tim@3: if (i+1) % 16 == 0: tim@3: print "%s: %s" % (line, repr(bytes[i-15:i+1])) tim@3: line = "" tim@3: tim@3: bytes_left = num % 16 tim@3: if bytes_left: tim@3: print "%s: %s" % (line.ljust(54), repr(bytes[-bytes_left:])) tim@3: tim@3: def getUrlStr(*args): tim@3: """Convert provided arguments to a string in 'host:port/prefix' format tim@3: Args can be: tim@3: - (host, port) tim@3: - (host, port), prefix tim@3: - host, port tim@3: - host, port, prefix tim@3: """ tim@3: if not len(args): tim@3: return "" tim@3: tim@3: if type(args[0]) == types.TupleType: tim@3: host = args[0][0] tim@3: port = args[0][1] tim@3: args = args[1:] tim@3: else: tim@3: host = args[0] tim@3: port = args[1] tim@3: args = args[2:] tim@3: tim@3: if len(args): tim@3: prefix = args[0] tim@3: else: tim@3: prefix = "" tim@3: tim@3: if len(host) and (host != '0.0.0.0'): tim@3: try: tim@3: (host, _, _) = socket.gethostbyaddr(host) tim@3: except socket.error: tim@3: pass tim@3: else: tim@3: host = 'localhost' tim@3: tim@3: if type(port) == types.IntType: tim@3: return "%s:%d%s" % (host, port, prefix) tim@3: else: tim@3: return host + prefix tim@3: tim@3: def parseUrlStr(url): tim@3: """Convert provided string in 'host:port/prefix' format to it's components tim@3: Returns ((host, port), prefix) tim@3: """ tim@3: if not (type(url) in types.StringTypes and len(url)): tim@3: return (None, '') tim@3: tim@3: i = url.find("://") tim@3: if i > -1: tim@3: url = url[i+3:] tim@3: tim@3: i = url.find(':') tim@3: if i > -1: tim@3: host = url[:i].strip() tim@3: tail = url[i+1:].strip() tim@3: else: tim@3: host = '' tim@3: tail = url tim@3: tim@3: for i in range(len(tail)): tim@3: if not tail[i].isdigit(): tim@3: break tim@3: else: tim@3: i += 1 tim@3: tim@3: portstr = tail[:i].strip() tim@3: tail = tail[i:].strip() tim@3: tim@3: found = len(tail) tim@3: for c in ('/', '+', '-', '*'): tim@3: i = tail.find(c) tim@3: if (i > -1) and (i < found): tim@3: found = i tim@3: tim@3: head = tail[:found].strip() tim@3: prefix = tail[found:].strip() tim@3: tim@3: prefix = prefix.strip('/') tim@3: if len(prefix) and prefix[0] not in ('+', '-', '*'): tim@3: prefix = '/' + prefix tim@3: tim@3: if len(head) and not len(host): tim@3: host = head tim@3: tim@3: if len(host): tim@3: try: tim@3: host = socket.gethostbyname(host) tim@3: except socket.error: tim@3: pass tim@3: tim@3: try: tim@3: port = int(portstr) tim@3: except ValueError: tim@3: port = None tim@3: tim@3: return ((host, port), prefix) tim@3: tim@3: ###### tim@3: # tim@3: # OSCClient class tim@3: # tim@3: ###### tim@3: tim@3: class OSCClient(object): tim@3: """Simple OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket tim@3: """ tim@3: # set outgoing socket buffer size tim@3: sndbuf_size = 4096 * 8 tim@3: tim@3: def __init__(self, server=None): tim@3: """Construct an OSC Client. tim@3: When the 'address' argument is given this client is connected to a specific remote server. tim@3: - address ((host, port) tuple): the address of the remote server to send all messages to tim@3: Otherwise it acts as a generic client: tim@3: If address == 'None', the client doesn't connect to a specific remote server, tim@3: and the remote address must be supplied when calling sendto() tim@3: - server: Local OSCServer-instance this client will use the socket of for transmissions. tim@3: If none is supplied, a socket will be created. tim@3: """ tim@3: self.socket = None tim@3: tim@3: if server == None: tim@3: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) tim@3: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) tim@3: self._fd = self.socket.fileno() tim@3: tim@3: self.server = None tim@3: else: tim@3: self.setServer(server) tim@3: tim@3: self.client_address = None tim@3: tim@3: def setServer(self, server): tim@3: """Associate this Client with given server. tim@3: The Client will send from the Server's socket. tim@3: The Server will use this Client instance to send replies. tim@3: """ tim@3: if not isinstance(server, OSCServer): tim@3: raise ValueError("'server' argument is not a valid OSCServer object") tim@3: tim@3: if self.socket != None: tim@3: self.close() tim@3: tim@3: self.socket = server.socket.dup() tim@3: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) tim@3: self._fd = self.socket.fileno() tim@3: tim@3: self.server = server tim@3: tim@3: if self.server.client != None: tim@3: self.server.client.close() tim@3: tim@3: self.server.client = self tim@3: tim@3: def close(self): tim@3: """Disconnect & close the Client's socket tim@3: """ tim@3: if self.socket != None: tim@3: self.socket.close() tim@3: self.socket = None tim@3: tim@3: def __str__(self): tim@3: """Returns a string containing this Client's Class-name, software-version tim@3: and the remote-address it is connected to (if any) tim@3: """ tim@3: out = self.__class__.__name__ tim@3: out += " v%s.%s-%s" % version tim@3: addr = self.address() tim@3: if addr: tim@3: out += " connected to osc://%s" % getUrlStr(addr) tim@3: else: tim@3: out += " (unconnected)" tim@3: tim@3: return out tim@3: tim@3: def __eq__(self, other): tim@3: """Compare function. tim@3: """ tim@3: if not isinstance(other, self.__class__): tim@3: return False tim@3: tim@3: isequal = cmp(self.socket._sock, other.socket._sock) tim@3: if isequal and self.server and other.server: tim@3: return cmp(self.server, other.server) tim@3: tim@3: return isequal tim@3: tim@3: def __ne__(self, other): tim@3: """Compare function. tim@3: """ tim@3: return not self.__eq__(other) tim@3: tim@3: def address(self): tim@3: """Returns a (host,port) tuple of the remote server this client is tim@3: connected to or None if not connected to any server. tim@3: """ tim@3: try: tim@3: return self.socket.getpeername() tim@3: except socket.error: tim@3: return None tim@3: tim@3: def connect(self, address): tim@3: """Bind to a specific OSC server: tim@3: the 'address' argument is a (host, port) tuple tim@3: - host: hostname of the remote OSC server, tim@3: - port: UDP-port the remote OSC server listens to. tim@3: """ tim@3: try: tim@3: self.socket.connect(address) tim@3: self.client_address = address tim@3: except socket.error, e: tim@3: self.client_address = None tim@3: raise OSCClientError("SocketError: %s" % str(e)) tim@3: tim@3: if self.server != None: tim@3: self.server.return_port = address[1] tim@3: tim@3: def sendto(self, msg, address, timeout=None): tim@3: """Send the given OSCMessage to the specified address. tim@3: - msg: OSCMessage (or OSCBundle) to be sent tim@3: - address: (host, port) tuple specifing remote server to send the message to tim@3: - timeout: A timeout value for attempting to send. If timeout == None, tim@3: this call blocks until socket is available for writing. tim@3: Raises OSCClientError when timing out while waiting for the socket. tim@3: """ tim@3: if not isinstance(msg, OSCMessage): tim@3: raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") tim@3: tim@3: ret = select.select([],[self._fd], [], timeout) tim@3: try: tim@3: ret[1].index(self._fd) tim@3: except: tim@3: # for the very rare case this might happen tim@3: raise OSCClientError("Timed out waiting for file descriptor") tim@3: tim@3: try: tim@3: self.socket.connect(address) tim@3: self.socket.sendall(msg.getBinary()) tim@3: tim@3: if self.client_address: tim@3: self.socket.connect(self.client_address) tim@3: tim@3: except socket.error, e: tim@3: if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' tim@3: raise e tim@3: #elif e[0]==61: tim@3: # pass # ADDED BY TIM: 61 = 'Connection refused.' tim@3: else: tim@3: raise OSCClientError("while sending to %s: %s" % (str(address), str(e))) tim@3: tim@3: def send(self, msg, timeout=None): tim@3: """Send the given OSCMessage. tim@3: The Client must be already connected. tim@3: - msg: OSCMessage (or OSCBundle) to be sent tim@3: - timeout: A timeout value for attempting to send. If timeout == None, tim@3: this call blocks until socket is available for writing. tim@3: Raises OSCClientError when timing out while waiting for the socket, tim@3: or when the Client isn't connected to a remote server. tim@3: """ tim@3: if not isinstance(msg, OSCMessage): tim@3: raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") tim@3: tim@3: ret = select.select([],[self._fd], [], timeout) tim@3: try: tim@3: ret[1].index(self._fd) tim@3: except: tim@3: # for the very rare case this might happen tim@3: raise OSCClientError("Timed out waiting for file descriptor") tim@3: tim@3: try: tim@3: self.socket.sendall(msg.getBinary()) tim@3: except socket.error, e: tim@3: if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' tim@3: raise e tim@3: else: tim@3: raise OSCClientError("while sending: %s" % str(e)) tim@3: tim@3: ###### tim@3: # tim@3: # FilterString Utility functions tim@3: # tim@3: ###### tim@3: tim@3: def parseFilterStr(args): tim@3: """Convert Message-Filter settings in '+ - ...' format to a dict of the form tim@3: { '':True, '':False, ... } tim@3: Returns a list: ['', filters] tim@3: """ tim@3: out = {} tim@3: tim@3: if type(args) in types.StringTypes: tim@3: args = [args] tim@3: tim@3: prefix = None tim@3: for arg in args: tim@3: head = None tim@3: for plus in arg.split('+'): tim@3: minus = plus.split('-') tim@3: plusfs = minus.pop(0).strip() tim@3: if len(plusfs): tim@3: plusfs = '/' + plusfs.strip('/') tim@3: tim@3: if (head == None) and (plusfs != "/*"): tim@3: head = plusfs tim@3: elif len(plusfs): tim@3: if plusfs == '/*': tim@3: out = { '/*':True } # reset all previous filters tim@3: else: tim@3: out[plusfs] = True tim@3: tim@3: for minusfs in minus: tim@3: minusfs = minusfs.strip() tim@3: if len(minusfs): tim@3: minusfs = '/' + minusfs.strip('/') tim@3: if minusfs == '/*': tim@3: out = { '/*':False } # reset all previous filters tim@3: else: tim@3: out[minusfs] = False tim@3: tim@3: if prefix == None: tim@3: prefix = head tim@3: tim@3: return [prefix, out] tim@3: tim@3: def getFilterStr(filters): tim@3: """Return the given 'filters' dict as a list of tim@3: '+' | '-' filter-strings tim@3: """ tim@3: if not len(filters): tim@3: return [] tim@3: tim@3: if '/*' in filters.keys(): tim@3: if filters['/*']: tim@3: out = ["+/*"] tim@3: else: tim@3: out = ["-/*"] tim@3: else: tim@3: if False in filters.values(): tim@3: out = ["+/*"] tim@3: else: tim@3: out = ["-/*"] tim@3: tim@3: for (addr, bool) in filters.items(): tim@3: if addr == '/*': tim@3: continue tim@3: tim@3: if bool: tim@3: out.append("+%s" % addr) tim@3: else: tim@3: out.append("-%s" % addr) tim@3: tim@3: return out tim@3: tim@3: # A translation-table for mapping OSC-address expressions to Python 're' expressions tim@3: OSCtrans = string.maketrans("{,}?","(|).") tim@3: tim@3: def getRegEx(pattern): tim@3: """Compiles and returns a 'regular expression' object for the given address-pattern. tim@3: """ tim@3: # Translate OSC-address syntax to python 're' syntax tim@3: pattern = pattern.replace(".", r"\.") # first, escape all '.'s in the pattern. tim@3: pattern = pattern.replace("(", r"\(") # escape all '('s. tim@3: pattern = pattern.replace(")", r"\)") # escape all ')'s. tim@3: pattern = pattern.replace("*", r".*") # replace a '*' by '.*' (match 0 or more characters) tim@3: pattern = pattern.translate(OSCtrans) # change '?' to '.' and '{,}' to '(|)' tim@3: tim@3: return re.compile(pattern) tim@3: tim@3: ###### tim@3: # tim@3: # OSCMultiClient class tim@3: # tim@3: ###### tim@3: tim@3: class OSCMultiClient(OSCClient): tim@3: """'Multiple-Unicast' OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket tim@3: This client keeps a dict of 'OSCTargets'. and sends each OSCMessage to each OSCTarget tim@3: The OSCTargets are simply (host, port) tuples, and may be associated with an OSC-address prefix. tim@3: the OSCTarget's prefix gets prepended to each OSCMessage sent to that target. tim@3: """ tim@3: def __init__(self, server=None): tim@3: """Construct a "Multi" OSC Client. tim@3: - server: Local OSCServer-instance this client will use the socket of for transmissions. tim@3: If none is supplied, a socket will be created. tim@3: """ tim@3: super(OSCMultiClient, self).__init__(server) tim@3: tim@3: self.targets = {} tim@3: tim@3: def _searchHostAddr(self, host): tim@3: """Search the subscribed OSCTargets for (the first occurence of) given host. tim@3: Returns a (host, port) tuple tim@3: """ tim@3: try: tim@3: host = socket.gethostbyname(host) tim@3: except socket.error: tim@3: pass tim@3: tim@3: for addr in self.targets.keys(): tim@3: if host == addr[0]: tim@3: return addr tim@3: tim@3: raise NotSubscribedError((host, None)) tim@3: tim@3: def _updateFilters(self, dst, src): tim@3: """Update a 'filters' dict with values form another 'filters' dict: tim@3: - src[a] == True and dst[a] == False: del dst[a] tim@3: - src[a] == False and dst[a] == True: del dst[a] tim@3: - a not in dst: dst[a] == src[a] tim@3: """ tim@3: if '/*' in src.keys(): # reset filters tim@3: dst.clear() # 'match everything' == no filters tim@3: if not src.pop('/*'): tim@3: dst['/*'] = False # 'match nothing' tim@3: tim@3: for (addr, bool) in src.items(): tim@3: if (addr in dst.keys()) and (dst[addr] != bool): tim@3: del dst[addr] tim@3: else: tim@3: dst[addr] = bool tim@3: tim@3: def _setTarget(self, address, prefix=None, filters=None): tim@3: """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget. tim@3: - address ((host, port) tuple): IP-address & UDP-port tim@3: - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage tim@3: sent to this OSCTarget (optional) tim@3: """ tim@3: if address not in self.targets.keys(): tim@3: self.targets[address] = ["",{}] tim@3: tim@3: if prefix != None: tim@3: if len(prefix): tim@3: # make sure prefix starts with ONE '/', and does not end with '/' tim@3: prefix = '/' + prefix.strip('/') tim@3: tim@3: self.targets[address][0] = prefix tim@3: tim@3: if filters != None: tim@3: if type(filters) in types.StringTypes: tim@3: (_, filters) = parseFilterStr(filters) tim@3: elif type(filters) != types.DictType: tim@3: raise TypeError("'filters' argument must be a dict with {addr:bool} entries") tim@3: tim@3: self._updateFilters(self.targets[address][1], filters) tim@3: tim@3: def setOSCTarget(self, address, prefix=None, filters=None): tim@3: """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget. tim@3: the 'address' argument can be a ((host, port) tuple) : The target server address & UDP-port tim@3: or a 'host' (string) : The host will be looked-up tim@3: - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage tim@3: sent to this OSCTarget (optional) tim@3: """ tim@3: if type(address) in types.StringTypes: tim@3: address = self._searchHostAddr(address) tim@3: tim@3: elif (type(address) == types.TupleType): tim@3: (host, port) = address[:2] tim@3: try: tim@3: host = socket.gethostbyname(host) tim@3: except: tim@3: pass tim@3: tim@3: address = (host, port) tim@3: else: tim@3: raise TypeError("'address' argument must be a (host, port) tuple or a 'host' string") tim@3: tim@3: self._setTarget(address, prefix, filters) tim@3: tim@3: def setOSCTargetFromStr(self, url): tim@3: """Adds or modifies a subscribed OSCTarget from the given string, which should be in the tim@3: ':[/] [+/]|[-/] ...' format. tim@3: """ tim@3: (addr, tail) = parseUrlStr(url) tim@3: (prefix, filters) = parseFilterStr(tail) tim@3: self._setTarget(addr, prefix, filters) tim@3: tim@3: def _delTarget(self, address, prefix=None): tim@3: """Delete the specified OSCTarget from the Client's dict. tim@3: the 'address' argument must be a (host, port) tuple. tim@3: If the 'prefix' argument is given, the Target is only deleted if the address and prefix match. tim@3: """ tim@3: try: tim@3: if prefix == None: tim@3: del self.targets[address] tim@3: elif prefix == self.targets[address][0]: tim@3: del self.targets[address] tim@3: except KeyError: tim@3: raise NotSubscribedError(address, prefix) tim@3: tim@3: def delOSCTarget(self, address, prefix=None): tim@3: """Delete the specified OSCTarget from the Client's dict. tim@3: the 'address' argument can be a ((host, port) tuple), or a hostname. tim@3: If the 'prefix' argument is given, the Target is only deleted if the address and prefix match. tim@3: """ tim@3: if type(address) in types.StringTypes: tim@3: address = self._searchHostAddr(address) tim@3: tim@3: if type(address) == types.TupleType: tim@3: (host, port) = address[:2] tim@3: try: tim@3: host = socket.gethostbyname(host) tim@3: except socket.error: tim@3: pass tim@3: address = (host, port) tim@3: tim@3: self._delTarget(address, prefix) tim@3: tim@3: def hasOSCTarget(self, address, prefix=None): tim@3: """Return True if the given OSCTarget exists in the Client's dict. tim@3: the 'address' argument can be a ((host, port) tuple), or a hostname. tim@3: If the 'prefix' argument is given, the return-value is only True if the address and prefix match. tim@3: """ tim@3: if type(address) in types.StringTypes: tim@3: address = self._searchHostAddr(address) tim@3: tim@3: if type(address) == types.TupleType: tim@3: (host, port) = address[:2] tim@3: try: tim@3: host = socket.gethostbyname(host) tim@3: except socket.error: tim@3: pass tim@3: address = (host, port) tim@3: tim@3: if address in self.targets.keys(): tim@3: if prefix == None: tim@3: return True tim@3: elif prefix == self.targets[address][0]: tim@3: return True tim@3: tim@3: return False tim@3: tim@3: def getOSCTargets(self): tim@3: """Returns the dict of OSCTargets: {addr:[prefix, filters], ...} tim@3: """ tim@3: out = {} tim@3: for ((host, port), pf) in self.targets.items(): tim@3: try: tim@3: (host, _, _) = socket.gethostbyaddr(host) tim@3: except socket.error: tim@3: pass tim@3: tim@3: out[(host, port)] = pf tim@3: tim@3: return out tim@3: tim@3: def getOSCTarget(self, address): tim@3: """Returns the OSCTarget matching the given address as a ((host, port), [prefix, filters]) tuple. tim@3: 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned tim@3: Returns (None, ['',{}]) if address not found. tim@3: """ tim@3: if type(address) in types.StringTypes: tim@3: address = self._searchHostAddr(address) tim@3: tim@3: if (type(address) == types.TupleType): tim@3: (host, port) = address[:2] tim@3: try: tim@3: host = socket.gethostbyname(host) tim@3: except socket.error: tim@3: pass tim@3: address = (host, port) tim@3: tim@3: if (address in self.targets.keys()): tim@3: try: tim@3: (host, _, _) = socket.gethostbyaddr(host) tim@3: except socket.error: tim@3: pass tim@3: tim@3: return ((host, port), self.targets[address]) tim@3: tim@3: return (None, ['',{}]) tim@3: tim@3: def clearOSCTargets(self): tim@3: """Erases all OSCTargets from the Client's dict tim@3: """ tim@3: self.targets = {} tim@3: tim@3: def updateOSCTargets(self, dict): tim@3: """Update the Client's OSCTargets dict with the contents of 'dict' tim@3: The given dict's items MUST be of the form tim@3: { (host, port):[prefix, filters], ... } tim@3: """ tim@3: for ((host, port), (prefix, filters)) in dict.items(): tim@3: val = [prefix, {}] tim@3: self._updateFilters(val[1], filters) tim@3: tim@3: try: tim@3: host = socket.gethostbyname(host) tim@3: except socket.error: tim@3: pass tim@3: tim@3: self.targets[(host, port)] = val tim@3: tim@3: def getOSCTargetStr(self, address): tim@3: """Returns the OSCTarget matching the given address as a ('osc://:[]', ['', ...])' tuple. tim@3: 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned tim@3: Returns (None, []) if address not found. tim@3: """ tim@3: (addr, (prefix, filters)) = self.getOSCTarget(address) tim@3: if addr == None: tim@3: return (None, []) tim@3: tim@3: return ("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters)) tim@3: tim@3: def getOSCTargetStrings(self): tim@3: """Returns a list of all OSCTargets as ('osc://:[]', ['', ...])' tuples. tim@3: """ tim@3: out = [] tim@3: for (addr, (prefix, filters)) in self.targets.items(): tim@3: out.append(("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters))) tim@3: tim@3: return out tim@3: tim@3: def connect(self, address): tim@3: """The OSCMultiClient isn't allowed to connect to any specific tim@3: address. tim@3: """ tim@3: return NotImplemented tim@3: tim@3: def sendto(self, msg, address, timeout=None): tim@3: """Send the given OSCMessage. tim@3: The specified address is ignored. Instead this method calls send() to tim@3: send the message to all subscribed clients. tim@3: - msg: OSCMessage (or OSCBundle) to be sent tim@3: - address: (host, port) tuple specifing remote server to send the message to tim@3: - timeout: A timeout value for attempting to send. If timeout == None, tim@3: this call blocks until socket is available for writing. tim@3: Raises OSCClientError when timing out while waiting for the socket. tim@3: """ tim@3: self.send(msg, timeout) tim@3: tim@3: def _filterMessage(self, filters, msg): tim@3: """Checks the given OSCMessge against the given filters. tim@3: 'filters' is a dict containing OSC-address:bool pairs. tim@3: If 'msg' is an OSCBundle, recursively filters its constituents. tim@3: Returns None if the message is to be filtered, else returns the message. tim@3: or tim@3: Returns a copy of the OSCBundle with the filtered messages removed. tim@3: """ tim@3: if isinstance(msg, OSCBundle): tim@3: out = msg.copy() tim@3: msgs = out.values() tim@3: out.clearData() tim@3: for m in msgs: tim@3: m = self._filterMessage(filters, m) tim@3: if m: # this catches 'None' and empty bundles. tim@3: out.append(m) tim@3: tim@3: elif isinstance(msg, OSCMessage): tim@3: if '/*' in filters.keys(): tim@3: if filters['/*']: tim@3: out = msg tim@3: else: tim@3: out = None tim@3: tim@3: elif False in filters.values(): tim@3: out = msg tim@3: else: tim@3: out = None tim@3: tim@3: else: tim@3: raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") tim@3: tim@3: expr = getRegEx(msg.address) tim@3: tim@3: for addr in filters.keys(): tim@3: if addr == '/*': tim@3: continue tim@3: tim@3: match = expr.match(addr) tim@3: if match and (match.end() == len(addr)): tim@3: if filters[addr]: tim@3: out = msg tim@3: else: tim@3: out = None tim@3: break tim@3: tim@3: return out tim@3: tim@3: def _prefixAddress(self, prefix, msg): tim@3: """Makes a copy of the given OSCMessage, then prepends the given prefix to tim@3: The message's OSC-address. tim@3: If 'msg' is an OSCBundle, recursively prepends the prefix to its constituents. tim@3: """ tim@3: out = msg.copy() tim@3: tim@3: if isinstance(msg, OSCBundle): tim@3: msgs = out.values() tim@3: out.clearData() tim@3: for m in msgs: tim@3: out.append(self._prefixAddress(prefix, m)) tim@3: tim@3: elif isinstance(msg, OSCMessage): tim@3: out.setAddress(prefix + out.address) tim@3: tim@3: else: tim@3: raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") tim@3: tim@3: return out tim@3: tim@3: def send(self, msg, timeout=None): tim@3: """Send the given OSCMessage to all subscribed OSCTargets tim@3: - msg: OSCMessage (or OSCBundle) to be sent tim@3: - timeout: A timeout value for attempting to send. If timeout == None, tim@3: this call blocks until socket is available for writing. tim@3: Raises OSCClientError when timing out while waiting for the socket. tim@3: """ tim@3: for (address, (prefix, filters)) in self.targets.items(): tim@3: if len(filters): tim@3: out = self._filterMessage(filters, msg) tim@3: if not out: # this catches 'None' and empty bundles. tim@3: continue tim@3: else: tim@3: out = msg tim@3: tim@3: if len(prefix): tim@3: out = self._prefixAddress(prefix, msg) tim@3: tim@3: binary = out.getBinary() tim@3: tim@3: ret = select.select([],[self._fd], [], timeout) tim@3: try: tim@3: ret[1].index(self._fd) tim@3: except: tim@3: # for the very rare case this might happen tim@3: raise OSCClientError("Timed out waiting for file descriptor") tim@3: tim@3: try: tim@3: while len(binary): tim@3: sent = self.socket.sendto(binary, address) tim@3: binary = binary[sent:] tim@3: tim@3: except socket.error, e: tim@3: if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' tim@3: raise e tim@3: else: tim@3: raise OSCClientError("while sending to %s: %s" % (str(address), str(e))) tim@3: tim@3: ###### tim@3: # tim@3: # OSCRequestHandler classes tim@3: # tim@3: ###### tim@3: tim@3: class OSCRequestHandler(DatagramRequestHandler): tim@3: """RequestHandler class for the OSCServer tim@3: """ tim@3: def dispatchMessage(self, pattern, tags, data): tim@3: """Attmept to match the given OSC-address pattern, which may contain '*', tim@3: against all callbacks registered with the OSCServer. tim@3: Calls the matching callback and returns whatever it returns. tim@3: If no match is found, and a 'default' callback is registered, it calls that one, tim@3: or raises NoCallbackError if a 'default' callback is not registered. tim@3: tim@3: - pattern (string): The OSC-address of the receied message tim@3: - tags (string): The OSC-typetags of the receied message's arguments, without ',' tim@3: - data (list): The message arguments tim@3: """ tim@3: if len(tags) != len(data): tim@3: raise OSCServerError("Malformed OSC-message; got %d typetags [%s] vs. %d values" % (len(tags), tags, len(data))) tim@3: tim@3: expr = getRegEx(pattern) tim@3: tim@3: replies = [] tim@3: matched = 0 tim@3: for addr in self.server.callbacks.keys(): tim@3: match = expr.match(addr) tim@3: if match and (match.end() == len(addr)): tim@3: reply = self.server.callbacks[addr](pattern, tags, data, self.client_address) tim@3: matched += 1 tim@3: if isinstance(reply, OSCMessage): tim@3: replies.append(reply) tim@3: elif reply != None: tim@3: raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks[addr], type(reply))) tim@3: tim@3: if matched == 0: tim@3: if 'default' in self.server.callbacks: tim@3: reply = self.server.callbacks['default'](pattern, tags, data, self.client_address) tim@3: if isinstance(reply, OSCMessage): tim@3: replies.append(reply) tim@3: elif reply != None: tim@3: raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks['default'], type(reply))) tim@3: else: tim@3: raise NoCallbackError(pattern) tim@3: tim@3: return replies tim@3: tim@3: def setup(self): tim@3: """Prepare RequestHandler. tim@3: Unpacks request as (packet, source socket address) tim@3: Creates an empty list for replies. tim@3: """ tim@3: (self.packet, self.socket) = self.request tim@3: self.replies = [] tim@3: tim@3: def _unbundle(self, decoded): tim@3: """Recursive bundle-unpacking function""" tim@3: if decoded[0] != "#bundle": tim@3: self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:]) tim@3: return tim@3: tim@3: now = time.time() tim@3: timetag = decoded[1] tim@3: if (timetag > 0.) and (timetag > now): tim@3: time.sleep(timetag - now) tim@3: tim@3: for msg in decoded[2:]: tim@3: self._unbundle(msg) tim@3: tim@3: def handle(self): tim@3: """Handle incoming OSCMessage tim@3: """ tim@3: decoded = decodeOSC(self.packet) tim@3: if not len(decoded): tim@3: return tim@3: tim@3: self._unbundle(decoded) tim@3: tim@3: def finish(self): tim@3: """Finish handling OSCMessage. tim@3: Send any reply returned by the callback(s) back to the originating client tim@3: as an OSCMessage or OSCBundle tim@3: """ tim@3: if self.server.return_port: tim@3: self.client_address = (self.client_address[0], self.server.return_port) tim@3: tim@3: if len(self.replies) > 1: tim@3: msg = OSCBundle() tim@3: for reply in self.replies: tim@3: msg.append(reply) tim@3: elif len(self.replies) == 1: tim@3: msg = self.replies[0] tim@3: else: tim@3: return tim@3: tim@3: self.server.client.sendto(msg, self.client_address) tim@3: tim@3: class ThreadingOSCRequestHandler(OSCRequestHandler): tim@3: """Multi-threaded OSCRequestHandler; tim@3: Starts a new RequestHandler thread for each unbundled OSCMessage tim@3: """ tim@3: def _unbundle(self, decoded): tim@3: """Recursive bundle-unpacking function tim@3: This version starts a new thread for each sub-Bundle found in the Bundle, tim@3: then waits for all its children to finish. tim@3: """ tim@3: if decoded[0] != "#bundle": tim@3: self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:]) tim@3: return tim@3: tim@3: now = time.time() tim@3: timetag = decoded[1] tim@3: if (timetag > 0.) and (timetag > now): tim@3: time.sleep(timetag - now) tim@3: now = time.time() tim@3: tim@3: children = [] tim@3: tim@3: for msg in decoded[2:]: tim@3: t = threading.Thread(target = self._unbundle, args = (msg,)) tim@3: t.start() tim@3: children.append(t) tim@3: tim@3: # wait for all children to terminate tim@3: for t in children: tim@3: t.join() tim@3: tim@3: ###### tim@3: # tim@3: # OSCServer classes tim@3: # tim@3: ###### tim@3: tim@3: class OSCServer(UDPServer): tim@3: """A Synchronous OSCServer tim@3: Serves one request at-a-time, until the OSCServer is closed. tim@3: The OSC address-pattern is matched against a set of OSC-adresses tim@3: that have been registered to the server with a callback-function. tim@3: If the adress-pattern of the message machtes the registered address of a callback, tim@3: that function is called. tim@3: """ tim@3: tim@3: # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer tim@3: RequestHandlerClass = OSCRequestHandler tim@3: tim@3: # define a socket timeout, so the serve_forever loop can actually exit. tim@3: socket_timeout = 1 tim@3: tim@3: # DEBUG: print error-tracebacks (to stderr)? tim@3: print_tracebacks = False tim@3: tim@3: def __init__(self, server_address, client=None, return_port=0): tim@3: """Instantiate an OSCServer. tim@3: - server_address ((host, port) tuple): the local host & UDP-port tim@3: the server listens on tim@3: - client (OSCClient instance): The OSCClient used to send replies from this server. tim@3: If none is supplied (default) an OSCClient will be created. tim@3: - return_port (int): if supplied, sets the default UDP destination-port tim@3: for replies coming from this server. tim@3: """ tim@3: UDPServer.__init__(self, server_address, self.RequestHandlerClass) tim@3: tim@3: self.callbacks = {} tim@3: self.setReturnPort(return_port) tim@3: self.error_prefix = "" tim@3: self.info_prefix = "/info" tim@3: tim@3: self.socket.settimeout(self.socket_timeout) tim@3: tim@3: self.running = False tim@3: self.client = None tim@3: tim@3: if client == None: tim@3: self.client = OSCClient(server=self) tim@3: else: tim@3: self.setClient(client) tim@3: tim@3: def setClient(self, client): tim@3: """Associate this Server with a new local Client instance, closing the Client this Server is currently using. tim@3: """ tim@3: if not isinstance(client, OSCClient): tim@3: raise ValueError("'client' argument is not a valid OSCClient object") tim@3: tim@3: if client.server != None: tim@3: raise OSCServerError("Provided OSCClient already has an OSCServer-instance: %s" % str(client.server)) tim@3: tim@3: # Server socket is already listening at this point, so we can't use the client's socket. tim@3: # we'll have to force our socket on the client... tim@3: client_address = client.address() # client may be already connected tim@3: client.close() # shut-down that socket tim@3: tim@3: # force our socket upon the client tim@3: client.socket = self.socket.dup() tim@3: client.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, client.sndbuf_size) tim@3: client._fd = client.socket.fileno() tim@3: client.server = self tim@3: tim@3: if client_address: tim@3: client.connect(client_address) tim@3: if not self.return_port: tim@3: self.return_port = client_address[1] tim@3: tim@3: if self.client != None: tim@3: self.client.close() tim@3: tim@3: self.client = client tim@3: tim@3: def serve_forever(self): tim@3: """Handle one request at a time until server is closed.""" tim@3: self.running = True tim@3: while self.running: tim@3: self.handle_request() # this times-out when no data arrives. tim@3: tim@3: def close(self): tim@3: """Stops serving requests, closes server (socket), closes used client tim@3: """ tim@3: self.running = False tim@3: self.client.close() tim@3: self.server_close() tim@3: tim@3: def __str__(self): tim@3: """Returns a string containing this Server's Class-name, software-version and local bound address (if any) tim@3: """ tim@3: out = self.__class__.__name__ tim@3: out += " v%s.%s-%s" % version tim@3: addr = self.address() tim@3: if addr: tim@3: out += " listening on osc://%s" % getUrlStr(addr) tim@3: else: tim@3: out += " (unbound)" tim@3: tim@3: return out tim@3: tim@3: def __eq__(self, other): tim@3: """Compare function. tim@3: """ tim@3: if not isinstance(other, self.__class__): tim@3: return False tim@3: tim@3: return cmp(self.socket._sock, other.socket._sock) tim@3: tim@3: def __ne__(self, other): tim@3: """Compare function. tim@3: """ tim@3: return not self.__eq__(other) tim@3: tim@3: def address(self): tim@3: """Returns a (host,port) tuple of the local address this server is bound to, tim@3: or None if not bound to any address. tim@3: """ tim@3: try: tim@3: return self.socket.getsockname() tim@3: except socket.error: tim@3: return None tim@3: tim@3: def setReturnPort(self, port): tim@3: """Set the destination UDP-port for replies returning from this server to the remote client tim@3: """ tim@3: if (port > 1024) and (port < 65536): tim@3: self.return_port = port tim@3: else: tim@3: self.return_port = None tim@3: tim@3: tim@3: def setSrvInfoPrefix(self, pattern): tim@3: """Set the first part of OSC-address (pattern) this server will use to reply to server-info requests. tim@3: """ tim@3: if len(pattern): tim@3: pattern = '/' + pattern.strip('/') tim@3: tim@3: self.info_prefix = pattern tim@3: tim@3: def setSrvErrorPrefix(self, pattern=""): tim@3: """Set the OSC-address (pattern) this server will use to report errors occuring during tim@3: received message handling to the remote client. tim@3: tim@3: If pattern is empty (default), server-errors are not reported back to the client. tim@3: """ tim@3: if len(pattern): tim@3: pattern = '/' + pattern.strip('/') tim@3: tim@3: self.error_prefix = pattern tim@3: tim@3: def addMsgHandler(self, address, callback): tim@3: """Register a handler for an OSC-address tim@3: - 'address' is the OSC address-string. tim@3: the address-string should start with '/' and may not contain '*' tim@3: - 'callback' is the function called for incoming OSCMessages that match 'address'. tim@3: The callback-function will be called with the same arguments as the 'msgPrinter_handler' below tim@3: """ tim@3: for chk in '*?,[]{}# ': tim@3: if chk in address: tim@3: raise OSCServerError("OSC-address string may not contain any characters in '*?,[]{}# '") tim@3: tim@3: if type(callback) not in (types.FunctionType, types.MethodType): tim@3: raise OSCServerError("Message callback '%s' is not callable" % repr(callback)) tim@3: tim@3: if address != 'default': tim@3: address = '/' + address.strip('/') tim@3: tim@3: self.callbacks[address] = callback tim@3: tim@3: def delMsgHandler(self,address): tim@3: """Remove the registered handler for the given OSC-address tim@3: """ tim@3: del self.callbacks[address] tim@3: tim@3: def getOSCAddressSpace(self): tim@3: """Returns a list containing all OSC-addresses registerd with this Server. tim@3: """ tim@3: return self.callbacks.keys() tim@3: tim@3: def addDefaultHandlers(self, prefix="", info_prefix="/info", error_prefix="/error"): tim@3: """Register a default set of OSC-address handlers with this Server: tim@3: - 'default' -> noCallback_handler tim@3: the given prefix is prepended to all other callbacks registered by this method: tim@3: - ' serverInfo_handler tim@3: - ' -> msgPrinter_handler tim@3: - '/print' -> msgPrinter_handler tim@3: and, if the used Client supports it; tim@3: - '/subscribe' -> subscription_handler tim@3: - '/unsubscribe' -> subscription_handler tim@3: tim@3: Note: the given 'error_prefix' argument is also set as default 'error_prefix' for error-messages tim@3: *sent from* this server. This is ok, because error-messages generally do not elicit a reply from the receiver. tim@3: tim@3: To do this with the serverInfo-prefixes would be a bad idea, because if a request received on '/info' (for example) tim@3: would send replies to '/info', this could potentially cause a never-ending loop of messages! tim@3: Do *not* set the 'info_prefix' here (for incoming serverinfo requests) to the same value as given to tim@3: the setSrvInfoPrefix() method (for *replies* to incoming serverinfo requests). tim@3: For example, use '/info' for incoming requests, and '/inforeply' or '/serverinfo' or even just '/print' as the tim@3: info-reply prefix. tim@3: """ tim@3: self.error_prefix = error_prefix tim@3: self.addMsgHandler('default', self.noCallback_handler) tim@3: self.addMsgHandler(prefix + info_prefix, self.serverInfo_handler) tim@3: self.addMsgHandler(prefix + error_prefix, self.msgPrinter_handler) tim@3: self.addMsgHandler(prefix + '/print', self.msgPrinter_handler) tim@3: tim@3: if isinstance(self.client, OSCMultiClient): tim@3: self.addMsgHandler(prefix + '/subscribe', self.subscription_handler) tim@3: self.addMsgHandler(prefix + '/unsubscribe', self.subscription_handler) tim@3: tim@3: def printErr(self, txt): tim@3: """Writes 'OSCServer: txt' to sys.stderr tim@3: """ tim@3: sys.stderr.write("OSCServer: %s\n" % txt) tim@3: tim@3: def sendOSCerror(self, txt, client_address): tim@3: """Sends 'txt', encapsulated in an OSCMessage to the default 'error_prefix' OSC-addres. tim@3: Message is sent to the given client_address, with the default 'return_port' overriding tim@3: the client_address' port, if defined. tim@3: """ tim@3: lines = txt.split('\n') tim@3: if len(lines) == 1: tim@3: msg = OSCMessage(self.error_prefix) tim@3: msg.append(lines[0]) tim@3: elif len(lines) > 1: tim@3: msg = OSCBundle(self.error_prefix) tim@3: for line in lines: tim@3: msg.append(line) tim@3: else: tim@3: return tim@3: tim@3: if self.return_port: tim@3: client_address = (client_address[0], self.return_port) tim@3: tim@3: self.client.sendto(msg, client_address) tim@3: tim@3: def reportErr(self, txt, client_address): tim@3: """Writes 'OSCServer: txt' to sys.stderr tim@3: If self.error_prefix is defined, sends 'txt' as an OSC error-message to the client(s) tim@3: (see printErr() and sendOSCerror()) tim@3: """ tim@3: self.printErr(txt) tim@3: tim@3: if len(self.error_prefix): tim@3: self.sendOSCerror(txt, client_address) tim@3: tim@3: def sendOSCinfo(self, txt, client_address): tim@3: """Sends 'txt', encapsulated in an OSCMessage to the default 'info_prefix' OSC-addres. tim@3: Message is sent to the given client_address, with the default 'return_port' overriding tim@3: the client_address' port, if defined. tim@3: """ tim@3: lines = txt.split('\n') tim@3: if len(lines) == 1: tim@3: msg = OSCMessage(self.info_prefix) tim@3: msg.append(lines[0]) tim@3: elif len(lines) > 1: tim@3: msg = OSCBundle(self.info_prefix) tim@3: for line in lines: tim@3: msg.append(line) tim@3: else: tim@3: return tim@3: tim@3: if self.return_port: tim@3: client_address = (client_address[0], self.return_port) tim@3: tim@3: self.client.sendto(msg, client_address) tim@3: tim@3: ### tim@3: # Message-Handler callback functions tim@3: ### tim@3: tim@3: def handle_error(self, request, client_address): tim@3: """Handle an exception in the Server's callbacks gracefully. tim@3: Writes the error to sys.stderr and, if the error_prefix (see setSrvErrorPrefix()) is set, tim@3: sends the error-message as reply to the client tim@3: """ tim@3: (e_type, e) = sys.exc_info()[:2] tim@3: if not str(e).endswith('[Errno 61] Connection refused'): # ADDED BY TIM tim@3: self.printErr("%s on request from %s: %s" % (e_type.__name__, getUrlStr(client_address), str(e))) tim@3: tim@3: if self.print_tracebacks: tim@3: import traceback tim@3: traceback.print_exc() # XXX But this goes to stderr! tim@3: tim@3: if len(self.error_prefix): tim@3: self.sendOSCerror("%s: %s" % (e_type.__name__, str(e)), client_address) tim@3: tim@3: def noCallback_handler(self, addr, tags, data, client_address): tim@3: """Example handler for OSCMessages. tim@3: All registerd handlers must accept these three arguments: tim@3: - addr (string): The OSC-address pattern of the received Message tim@3: (the 'addr' string has already been matched against the handler's registerd OSC-address, tim@3: but may contain '*'s & such) tim@3: - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) tim@3: - data (list): The OSCMessage's arguments tim@3: Note that len(tags) == len(data) tim@3: - client_address ((host, port) tuple): the host & port this message originated from. tim@3: tim@3: a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), tim@3: which then gets sent back to the client. tim@3: tim@3: This handler prints a "No callback registered to handle ..." message. tim@3: Returns None tim@3: """ tim@3: self.reportErr("No callback registered to handle OSC-address '%s'" % addr, client_address) tim@3: tim@3: def msgPrinter_handler(self, addr, tags, data, client_address): tim@3: """Example handler for OSCMessages. tim@3: All registerd handlers must accept these three arguments: tim@3: - addr (string): The OSC-address pattern of the received Message tim@3: (the 'addr' string has already been matched against the handler's registerd OSC-address, tim@3: but may contain '*'s & such) tim@3: - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) tim@3: - data (list): The OSCMessage's arguments tim@3: Note that len(tags) == len(data) tim@3: - client_address ((host, port) tuple): the host & port this message originated from. tim@3: tim@3: a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), tim@3: which then gets sent back to the client. tim@3: tim@3: This handler prints the received message. tim@3: Returns None tim@3: """ tim@3: txt = "OSCMessage '%s' from %s: " % (addr, getUrlStr(client_address)) tim@3: txt += str(data) tim@3: tim@3: self.printErr(txt) # strip trailing comma & space tim@3: tim@3: def serverInfo_handler(self, addr, tags, data, client_address): tim@3: """Example handler for OSCMessages. tim@3: All registerd handlers must accept these three arguments: tim@3: - addr (string): The OSC-address pattern of the received Message tim@3: (the 'addr' string has already been matched against the handler's registerd OSC-address, tim@3: but may contain '*'s & such) tim@3: - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) tim@3: - data (list): The OSCMessage's arguments tim@3: Note that len(tags) == len(data) tim@3: - client_address ((host, port) tuple): the host & port this message originated from. tim@3: tim@3: a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), tim@3: which then gets sent back to the client. tim@3: tim@3: This handler returns a reply to the client, which can contain various bits of information tim@3: about this server, depending on the first argument of the received OSC-message: tim@3: - 'help' | 'info' : Reply contains server type & version info, plus a list of tim@3: available 'commands' understood by this handler tim@3: - 'list' | 'ls' : Reply is a bundle of 'address ' messages, listing the server's tim@3: OSC address-space. tim@3: - 'clients' | 'targets' : Reply is a bundle of 'target osc://:[] [] [...]' tim@3: messages, listing the local Client-instance's subscribed remote clients. tim@3: """ tim@3: if len(data) == 0: tim@3: return None tim@3: tim@3: cmd = data.pop(0) tim@3: tim@3: reply = None tim@3: if cmd in ('help', 'info'): tim@3: reply = OSCBundle(self.info_prefix) tim@3: reply.append(('server', str(self))) tim@3: reply.append(('info_command', "ls | list : list OSC address-space")) tim@3: reply.append(('info_command', "clients | targets : list subscribed clients")) tim@3: elif cmd in ('ls', 'list'): tim@3: reply = OSCBundle(self.info_prefix) tim@3: for addr in self.callbacks.keys(): tim@3: reply.append(('address', addr)) tim@3: elif cmd in ('clients', 'targets'): tim@3: if hasattr(self.client, 'getOSCTargetStrings'): tim@3: reply = OSCBundle(self.info_prefix) tim@3: for trg in self.client.getOSCTargetStrings(): tim@3: reply.append(('target',) + trg) tim@3: else: tim@3: cli_addr = self.client.address() tim@3: if cli_addr: tim@3: reply = OSCMessage(self.info_prefix) tim@3: reply.append(('target', "osc://%s/" % getUrlStr(cli_addr))) tim@3: else: tim@3: self.reportErr("unrecognized command '%s' in /info request from osc://%s. Try 'help'" % (cmd, getUrlStr(client_address)), client_address) tim@3: tim@3: return reply tim@3: tim@3: def _subscribe(self, data, client_address): tim@3: """Handle the actual subscription. the provided 'data' is concatenated together to form a tim@3: ':[] [] [...]' string, which is then passed to tim@3: parseUrlStr() & parseFilterStr() to actually retreive , , etc. tim@3: tim@3: This 'long way 'round' approach (almost) guarantees that the subscription works, tim@3: regardless of how the bits of the are encoded in 'data'. tim@3: """ tim@3: url = "" tim@3: have_port = False tim@3: for item in data: tim@3: if (type(item) == types.IntType) and not have_port: tim@3: url += ":%d" % item tim@3: have_port = True tim@3: elif type(item) in types.StringTypes: tim@3: url += item tim@3: tim@3: (addr, tail) = parseUrlStr(url) tim@3: (prefix, filters) = parseFilterStr(tail) tim@3: tim@3: if addr != None: tim@3: (host, port) = addr tim@3: if not host: tim@3: host = client_address[0] tim@3: if not port: tim@3: port = client_address[1] tim@3: addr = (host, port) tim@3: else: tim@3: addr = client_address tim@3: tim@3: self.client._setTarget(addr, prefix, filters) tim@3: tim@3: trg = self.client.getOSCTargetStr(addr) tim@3: if trg[0] != None: tim@3: reply = OSCMessage(self.info_prefix) tim@3: reply.append(('target',) + trg) tim@3: return reply tim@3: tim@3: def _unsubscribe(self, data, client_address): tim@3: """Handle the actual unsubscription. the provided 'data' is concatenated together to form a tim@3: ':[]' string, which is then passed to tim@3: parseUrlStr() to actually retreive , & . tim@3: tim@3: This 'long way 'round' approach (almost) guarantees that the unsubscription works, tim@3: regardless of how the bits of the are encoded in 'data'. tim@3: """ tim@3: url = "" tim@3: have_port = False tim@3: for item in data: tim@3: if (type(item) == types.IntType) and not have_port: tim@3: url += ":%d" % item tim@3: have_port = True tim@3: elif type(item) in types.StringTypes: tim@3: url += item tim@3: tim@3: (addr, _) = parseUrlStr(url) tim@3: tim@3: if addr == None: tim@3: addr = client_address tim@3: else: tim@3: (host, port) = addr tim@3: if not host: tim@3: host = client_address[0] tim@3: if not port: tim@3: try: tim@3: (host, port) = self.client._searchHostAddr(host) tim@3: except NotSubscribedError: tim@3: port = client_address[1] tim@3: tim@3: addr = (host, port) tim@3: tim@3: try: tim@3: self.client._delTarget(addr) tim@3: except NotSubscribedError, e: tim@3: txt = "%s: %s" % (e.__class__.__name__, str(e)) tim@3: self.printErr(txt) tim@3: tim@3: reply = OSCMessage(self.error_prefix) tim@3: reply.append(txt) tim@3: return reply tim@3: tim@3: def subscription_handler(self, addr, tags, data, client_address): tim@3: """Handle 'subscribe' / 'unsubscribe' requests from remote hosts, tim@3: if the local Client supports this (i.e. OSCMultiClient). tim@3: tim@3: Supported commands: tim@3: - 'help' | 'info' : Reply contains server type & version info, plus a list of tim@3: available 'commands' understood by this handler tim@3: - 'list' | 'ls' : Reply is a bundle of 'target osc://:[] [] [...]' tim@3: messages, listing the local Client-instance's subscribed remote clients. tim@3: - '[subscribe | listen | sendto | target] [ ...] : Subscribe remote client/server at , tim@3: and/or set message-filters for messages being sent to the subscribed host, with the optional tim@3: arguments. Filters are given as OSC-addresses (or '*') prefixed by a '+' (send matching messages) or tim@3: a '-' (don't send matching messages). The wildcard '*', '+*' or '+/*' means 'send all' / 'filter none', tim@3: and '-*' or '-/*' means 'send none' / 'filter all' (which is not the same as unsubscribing!) tim@3: Reply is an OSCMessage with the (new) subscription; 'target osc://:[] [] [...]' tim@3: - '[unsubscribe | silence | nosend | deltarget] : Unsubscribe remote client/server at tim@3: If the given isn't subscribed, a NotSubscribedError-message is printed (and possibly sent) tim@3: tim@3: The given to the subscribe/unsubscribe handler should be of the form: tim@3: '[osc://][][:][]', where any or all components can be omitted. tim@3: tim@3: If is not specified, the IP-address of the message's source is used. tim@3: If is not specified, the is first looked up in the list of subscribed hosts, and if found, tim@3: the associated port is used. tim@3: If is not specified and is not yet subscribed, the message's source-port is used. tim@3: If is specified on subscription, is prepended to the OSC-address of all messages tim@3: sent to the subscribed host. tim@3: If is specified on unsubscription, the subscribed host is only unsubscribed if the host, tim@3: port and prefix all match the subscription. tim@3: If is not specified on unsubscription, the subscribed host is unsubscribed if the host and port tim@3: match the subscription. tim@3: """ tim@3: if not isinstance(self.client, OSCMultiClient): tim@3: raise OSCServerError("Local %s does not support subsctiptions or message-filtering" % self.client.__class__.__name__) tim@3: tim@3: addr_cmd = addr.split('/')[-1] tim@3: tim@3: if len(data): tim@3: if data[0] in ('help', 'info'): tim@3: reply = OSCBundle(self.info_prefix) tim@3: reply.append(('server', str(self))) tim@3: reply.append(('subscribe_command', "ls | list : list subscribed targets")) tim@3: reply.append(('subscribe_command', "[subscribe | listen | sendto | target] [ ...] : subscribe to messages, set filters")) tim@3: reply.append(('subscribe_command', "[unsubscribe | silence | nosend | deltarget] : unsubscribe from messages")) tim@3: return reply tim@3: tim@3: if data[0] in ('ls', 'list'): tim@3: reply = OSCBundle(self.info_prefix) tim@3: for trg in self.client.getOSCTargetStrings(): tim@3: reply.append(('target',) + trg) tim@3: return reply tim@3: tim@3: if data[0] in ('subscribe', 'listen', 'sendto', 'target'): tim@3: return self._subscribe(data[1:], client_address) tim@3: tim@3: if data[0] in ('unsubscribe', 'silence', 'nosend', 'deltarget'): tim@3: return self._unsubscribe(data[1:], client_address) tim@3: tim@3: if addr_cmd in ('subscribe', 'listen', 'sendto', 'target'): tim@3: return self._subscribe(data, client_address) tim@3: tim@3: if addr_cmd in ('unsubscribe', 'silence', 'nosend', 'deltarget'): tim@3: return self._unsubscribe(data, client_address) tim@3: tim@3: class ForkingOSCServer(ForkingMixIn, OSCServer): tim@3: """An Asynchronous OSCServer. tim@3: This server forks a new process to handle each incoming request. tim@3: """ tim@3: # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer tim@3: RequestHandlerClass = ThreadingOSCRequestHandler tim@3: tim@3: class ThreadingOSCServer(ThreadingMixIn, OSCServer): tim@3: """An Asynchronous OSCServer. tim@3: This server starts a new thread to handle each incoming request. tim@3: """ tim@3: # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer tim@3: RequestHandlerClass = ThreadingOSCRequestHandler tim@3: tim@3: ###### tim@3: # tim@3: # OSCError classes tim@3: # tim@3: ###### tim@3: tim@3: class OSCError(Exception): tim@3: """Base Class for all OSC-related errors tim@3: """ tim@3: def __init__(self, message): tim@3: self.message = message tim@3: tim@3: def __str__(self): tim@3: return self.message tim@3: tim@3: class OSCClientError(OSCError): tim@3: """Class for all OSCClient errors tim@3: """ tim@3: pass tim@3: tim@3: class OSCServerError(OSCError): tim@3: """Class for all OSCServer errors tim@3: """ tim@3: pass tim@3: tim@3: class NoCallbackError(OSCServerError): tim@3: """This error is raised (by an OSCServer) when an OSCMessage with an 'unmatched' address-pattern tim@3: is received, and no 'default' handler is registered. tim@3: """ tim@3: def __init__(self, pattern): tim@3: """The specified 'pattern' should be the OSC-address of the 'unmatched' message causing the error to be raised. tim@3: """ tim@3: self.message = "No callback registered to handle OSC-address '%s'" % pattern tim@3: tim@3: class NotSubscribedError(OSCClientError): tim@3: """This error is raised (by an OSCMultiClient) when an attempt is made to unsubscribe a host tim@3: that isn't subscribed. tim@3: """ tim@3: def __init__(self, addr, prefix=None): tim@3: if prefix: tim@3: url = getUrlStr(addr, prefix) tim@3: else: tim@3: url = getUrlStr(addr, '') tim@3: tim@3: self.message = "Target osc://%s is not subscribed" % url tim@3: tim@3: ###### tim@3: # tim@3: # Testing Program tim@3: # tim@3: ###### tim@3: tim@3: tim@3: if __name__ == "__main__": tim@3: import optparse tim@3: tim@3: default_port = 2222 tim@3: tim@3: # define command-line options tim@3: op = optparse.OptionParser(description="OSC.py OpenSoundControl-for-Python Test Program") tim@3: op.add_option("-l", "--listen", dest="listen", tim@3: help="listen on given host[:port]. default = '0.0.0.0:%d'" % default_port) tim@3: op.add_option("-s", "--sendto", dest="sendto", tim@3: help="send to given host[:port]. default = '127.0.0.1:%d'" % default_port) tim@3: op.add_option("-t", "--threading", action="store_true", dest="threading", tim@3: help="Test ThreadingOSCServer") tim@3: op.add_option("-f", "--forking", action="store_true", dest="forking", tim@3: help="Test ForkingOSCServer") tim@3: op.add_option("-u", "--usage", action="help", help="show this help message and exit") tim@3: tim@3: op.set_defaults(listen=":%d" % default_port) tim@3: op.set_defaults(sendto="") tim@3: op.set_defaults(threading=False) tim@3: op.set_defaults(forking=False) tim@3: tim@3: # Parse args tim@3: (opts, args) = op.parse_args() tim@3: tim@3: addr, server_prefix = parseUrlStr(opts.listen) tim@3: if addr != None and addr[0] != None: tim@3: if addr[1] != None: tim@3: listen_address = addr tim@3: else: tim@3: listen_address = (addr[0], default_port) tim@3: else: tim@3: listen_address = ('', default_port) tim@3: tim@3: targets = {} tim@3: for trg in opts.sendto.split(','): tim@3: (addr, prefix) = parseUrlStr(trg) tim@3: if len(prefix): tim@3: (prefix, filters) = parseFilterStr(prefix) tim@3: else: tim@3: filters = {} tim@3: tim@3: if addr != None: tim@3: if addr[1] != None: tim@3: targets[addr] = [prefix, filters] tim@3: else: tim@3: targets[(addr[0], listen_address[1])] = [prefix, filters] tim@3: elif len(prefix) or len(filters): tim@3: targets[listen_address] = [prefix, filters] tim@3: tim@3: welcome = "Welcome to the OSC testing program." tim@3: print welcome tim@3: hexDump(welcome) tim@3: print tim@3: message = OSCMessage() tim@3: message.setAddress("/print") tim@3: message.append(44) tim@3: message.append(11) tim@3: message.append(4.5) tim@3: message.append("the white cliffs of dover") tim@3: tim@3: print message tim@3: hexDump(message.getBinary()) tim@3: tim@3: print "\nMaking and unmaking a message.." tim@3: tim@3: strings = OSCMessage("/prin{ce,t}") tim@3: strings.append("Mary had a little lamb") tim@3: strings.append("its fleece was white as snow") tim@3: strings.append("and everywhere that Mary went,") tim@3: strings.append("the lamb was sure to go.") tim@3: strings.append(14.5) tim@3: strings.append(14.5) tim@3: strings.append(-400) tim@3: tim@3: raw = strings.getBinary() tim@3: tim@3: print strings tim@3: hexDump(raw) tim@3: tim@3: print "Retrieving arguments..." tim@3: data = raw tim@3: for i in range(6): tim@3: text, data = _readString(data) tim@3: print text tim@3: tim@3: number, data = _readFloat(data) tim@3: print number tim@3: tim@3: number, data = _readFloat(data) tim@3: print number tim@3: tim@3: number, data = _readInt(data) tim@3: print number tim@3: tim@3: print decodeOSC(raw) tim@3: tim@3: print "\nTesting Blob types." tim@3: tim@3: blob = OSCMessage("/pri*") tim@3: blob.append("","b") tim@3: blob.append("b","b") tim@3: blob.append("bl","b") tim@3: blob.append("blo","b") tim@3: blob.append("blob","b") tim@3: blob.append("blobs","b") tim@3: blob.append(42) tim@3: tim@3: print blob tim@3: hexDump(blob.getBinary()) tim@3: tim@3: print1 = OSCMessage() tim@3: print1.setAddress("/print") tim@3: print1.append("Hey man, that's cool.") tim@3: print1.append(42) tim@3: print1.append(3.1415926) tim@3: tim@3: print "\nTesting OSCBundle" tim@3: tim@3: bundle = OSCBundle() tim@3: bundle.append(print1) tim@3: bundle.append({'addr':"/print", 'args':["bundled messages:", 2]}) tim@3: bundle.setAddress("/*print") tim@3: bundle.append(("no,", 3, "actually.")) tim@3: tim@3: print bundle tim@3: hexDump(bundle.getBinary()) tim@3: tim@3: # Instantiate OSCClient tim@3: print "\nInstantiating OSCClient:" tim@3: if len(targets): tim@3: c = OSCMultiClient() tim@3: c.updateOSCTargets(targets) tim@3: else: tim@3: c = OSCClient() tim@3: c.connect(listen_address) # connect back to our OSCServer tim@3: tim@3: print c tim@3: if hasattr(c, 'getOSCTargetStrings'): tim@3: print "Sending to:" tim@3: for (trg, filterstrings) in c.getOSCTargetStrings(): tim@3: out = trg tim@3: for fs in filterstrings: tim@3: out += " %s" % fs tim@3: tim@3: print out tim@3: tim@3: # Now an OSCServer... tim@3: print "\nInstantiating OSCServer:" tim@3: tim@3: # define a message-handler function for the server to call. tim@3: def printing_handler(addr, tags, stuff, source): tim@3: msg_string = "%s [%s] %s" % (addr, tags, str(stuff)) tim@3: sys.stdout.write("OSCServer Got: '%s' from %s\n" % (msg_string, getUrlStr(source))) tim@3: tim@3: # send a reply to the client. tim@3: msg = OSCMessage("/printed") tim@3: msg.append(msg_string) tim@3: return msg tim@3: tim@3: if opts.threading: tim@3: s = ThreadingOSCServer(listen_address, c, return_port=listen_address[1]) tim@3: elif opts.forking: tim@3: s = ForkingOSCServer(listen_address, c, return_port=listen_address[1]) tim@3: else: tim@3: s = OSCServer(listen_address, c, return_port=listen_address[1]) tim@3: tim@3: print s tim@3: tim@3: # Set Server to return errors as OSCMessages to "/error" tim@3: s.setSrvErrorPrefix("/error") tim@3: # Set Server to reply to server-info requests with OSCMessages to "/serverinfo" tim@3: s.setSrvInfoPrefix("/serverinfo") tim@3: tim@3: # this registers a 'default' handler (for unmatched messages), tim@3: # an /'error' handler, an '/info' handler. tim@3: # And, if the client supports it, a '/subscribe' & '/unsubscribe' handler tim@3: s.addDefaultHandlers() tim@3: tim@3: s.addMsgHandler("/print", printing_handler) tim@3: tim@3: # if client & server are bound to 'localhost', server replies return to itself! tim@3: s.addMsgHandler("/printed", s.msgPrinter_handler) tim@3: s.addMsgHandler("/serverinfo", s.msgPrinter_handler) tim@3: tim@3: print "Registered Callback-functions:" tim@3: for addr in s.getOSCAddressSpace(): tim@3: print addr tim@3: tim@3: print "\nStarting OSCServer. Use ctrl-C to quit." tim@3: st = threading.Thread(target=s.serve_forever) tim@3: st.start() tim@3: tim@3: if hasattr(c, 'targets') and listen_address not in c.targets.keys(): tim@3: print "\nSubscribing local Server to local Client" tim@3: c2 = OSCClient() tim@3: c2.connect(listen_address) tim@3: subreq = OSCMessage("/subscribe") tim@3: subreq.append(listen_address) tim@3: tim@3: print "sending: ", subreq tim@3: c2.send(subreq) tim@3: c2.close() tim@3: tim@3: time.sleep(0.1) tim@3: tim@3: print "\nRequesting OSC-address-space and subscribed clients from OSCServer" tim@3: inforeq = OSCMessage("/info") tim@3: for cmd in ("info", "list", "clients"): tim@3: inforeq.clearData() tim@3: inforeq.append(cmd) tim@3: tim@3: print "sending: ", inforeq tim@3: c.send(inforeq) tim@3: tim@3: time.sleep(0.1) tim@3: tim@3: print2 = print1.copy() tim@3: print2.setAddress('/noprint') tim@3: tim@3: print "\nSending Messages" tim@3: tim@3: for m in (message, print1, print2, strings, bundle): tim@3: print "sending: ", m tim@3: c.send(m) tim@3: tim@3: time.sleep(0.1) tim@3: tim@3: print "\nThe next message's address will match both the '/print' and '/printed' handlers..." tim@3: print "sending: ", blob tim@3: c.send(blob) tim@3: tim@3: time.sleep(0.1) tim@3: tim@3: print "\nBundles can be given a timestamp.\nThe receiving server should 'hold' the bundle until its time has come" tim@3: tim@3: waitbundle = OSCBundle("/print") tim@3: waitbundle.setTimeTag(time.time() + 5) tim@3: if s.__class__ == OSCServer: tim@3: waitbundle.append("Note how the (single-thread) OSCServer blocks while holding this bundle") tim@3: else: tim@3: waitbundle.append("Note how the %s does not block while holding this bundle" % s.__class__.__name__) tim@3: tim@3: print "Set timetag 5 s into the future" tim@3: print "sending: ", waitbundle tim@3: c.send(waitbundle) tim@3: tim@3: time.sleep(0.1) tim@3: tim@3: print "Recursing bundles, with timetags set to 10 s [25 s, 20 s, 10 s]" tim@3: bb = OSCBundle("/print") tim@3: bb.setTimeTag(time.time() + 10) tim@3: tim@3: b = OSCBundle("/print") tim@3: b.setTimeTag(time.time() + 25) tim@3: b.append("held for 25 sec") tim@3: bb.append(b) tim@3: tim@3: b.clearData() tim@3: b.setTimeTag(time.time() + 20) tim@3: b.append("held for 20 sec") tim@3: bb.append(b) tim@3: tim@3: b.clearData() tim@3: b.setTimeTag(time.time() + 15) tim@3: b.append("held for 15 sec") tim@3: bb.append(b) tim@3: tim@3: if s.__class__ == OSCServer: tim@3: bb.append("Note how the (single-thread) OSCServer handles the bundle's contents in order of appearance") tim@3: else: tim@3: bb.append("Note how the %s handles the sub-bundles in the order dictated by their timestamps" % s.__class__.__name__) tim@3: bb.append("Each bundle's contents, however, are processed in random order (dictated by the kernel's threading)") tim@3: tim@3: print "sending: ", bb tim@3: c.send(bb) tim@3: tim@3: time.sleep(0.1) tim@3: tim@3: print "\nMessages sent!" tim@3: tim@3: print "\nWaiting for OSCServer. Use ctrl-C to quit.\n" tim@3: tim@3: try: tim@3: while True: tim@3: time.sleep(30) tim@3: tim@3: except KeyboardInterrupt: tim@3: print "\nClosing OSCServer." tim@3: s.close() tim@3: print "Waiting for Server-thread to finish" tim@3: st.join() tim@3: print "Closing OSCClient" tim@3: c.close() tim@3: print "Done" tim@3: tim@3: sys.exit(0)