tim@3
|
1 #!/usr/bin/python
|
tim@3
|
2 """
|
tim@3
|
3 This module contains an OpenSoundControl implementation (in Pure Python), based (somewhat) on the
|
tim@3
|
4 good old 'SimpleOSC' implementation by Daniel Holth & Clinton McChesney.
|
tim@3
|
5
|
tim@3
|
6 This implementation is intended to still be 'Simple' to the user, but much more complete
|
tim@3
|
7 (with OSCServer & OSCClient classes) and much more powerful
|
tim@3
|
8 (the OSCMultiClient supports subscriptions & message-filtering,
|
tim@3
|
9 OSCMessage & OSCBundle are now proper container-types)
|
tim@3
|
10
|
tim@3
|
11 ================
|
tim@3
|
12 OpenSoundControl
|
tim@3
|
13 ================
|
tim@3
|
14
|
tim@3
|
15 OpenSoundControl is a network-protocol for sending (small) packets of addressed data over network sockets.
|
tim@3
|
16 This OSC-implementation uses the UDP/IP protocol for sending and receiving packets.
|
tim@3
|
17 (Although it is theoretically possible to send OSC-packets over TCP, almost all known implementations use UDP)
|
tim@3
|
18
|
tim@3
|
19 OSC-packets come in two kinds:
|
tim@3
|
20 - OSC-messages consist of an 'address'-string (not to be confused with a (host:port) network-address!),
|
tim@3
|
21 followed by a string of 'typetags' associated with the message's arguments (ie. 'payload'),
|
tim@3
|
22 and finally the arguments themselves, encoded in an OSC-specific way.
|
tim@3
|
23 The OSCMessage class makes it easy to create & manipulate OSC-messages of this kind in a 'pythonesque' way
|
tim@3
|
24 (that is, OSCMessage-objects behave a lot like lists)
|
tim@3
|
25
|
tim@3
|
26 - OSC-bundles are a special type of OSC-message containing only OSC-messages as 'payload'. Recursively.
|
tim@3
|
27 (meaning; an OSC-bundle could contain other OSC-bundles, containing OSC-bundles etc.)
|
tim@3
|
28 OSC-bundles start with the special keyword '#bundle' and do not have an OSC-address. (but the OSC-messages
|
tim@3
|
29 a bundle contains will have OSC-addresses!)
|
tim@3
|
30 Also, an OSC-bundle can have a timetag, essentially telling the receiving Server to 'hold' the bundle until
|
tim@3
|
31 the specified time.
|
tim@3
|
32 The OSCBundle class allows easy cration & manipulation of OSC-bundles.
|
tim@3
|
33
|
tim@3
|
34 see also http://opensoundcontrol.org/spec-1_0
|
tim@3
|
35
|
tim@3
|
36 ---------
|
tim@3
|
37
|
tim@3
|
38 To send OSC-messages, you need an OSCClient, and to receive OSC-messages you need an OSCServer.
|
tim@3
|
39
|
tim@3
|
40 The OSCClient uses an 'AF_INET / SOCK_DGRAM' type socket (see the 'socket' module) to send
|
tim@3
|
41 binary representations of OSC-messages to a remote host:port address.
|
tim@3
|
42
|
tim@3
|
43 The OSCServer listens on an 'AF_INET / SOCK_DGRAM' type socket bound to a local port, and handles
|
tim@3
|
44 incoming requests. Either one-after-the-other (OSCServer) or in a multi-threaded / multi-process fashion
|
tim@3
|
45 (ThreadingOSCServer / ForkingOSCServer). If the Server has a callback-function (a.k.a. handler) registered
|
tim@3
|
46 to 'deal with' (i.e. handle) the received message's OSC-address, that function is called, passing it the (decoded) message
|
tim@3
|
47
|
tim@3
|
48 The different OSCServers implemented here all support the (recursive) un-bundling of OSC-bundles,
|
tim@3
|
49 and OSC-bundle timetags.
|
tim@3
|
50
|
tim@3
|
51 In fact, this implementation supports:
|
tim@3
|
52
|
tim@3
|
53 - OSC-messages with 'i' (int32), 'f' (float32), 's' (string) and 'b' (blob / binary data) types
|
tim@3
|
54 - OSC-bundles, including timetag-support
|
tim@3
|
55 - OSC-address patterns including '*', '?', '{,}' and '[]' wildcards.
|
tim@3
|
56
|
tim@3
|
57 (please *do* read the OSC-spec! http://opensoundcontrol.org/spec-1_0 it explains what these things mean.)
|
tim@3
|
58
|
tim@3
|
59 In addition, the OSCMultiClient supports:
|
tim@3
|
60 - Sending a specific OSC-message to multiple remote servers
|
tim@3
|
61 - Remote server subscription / unsubscription (through OSC-messages, of course)
|
tim@3
|
62 - Message-address filtering.
|
tim@3
|
63
|
tim@3
|
64 ---------
|
tim@3
|
65
|
tim@3
|
66 Stock, V2_Lab, Rotterdam, 2008
|
tim@3
|
67
|
tim@3
|
68 ----------
|
tim@3
|
69 Changelog:
|
tim@3
|
70 ----------
|
tim@3
|
71 v0.3.0 - 27 Dec. 2007
|
tim@3
|
72 Started out to extend the 'SimpleOSC' implementation (v0.2.3) by Daniel Holth & Clinton McChesney.
|
tim@3
|
73 Rewrote OSCMessage
|
tim@3
|
74 Added OSCBundle
|
tim@3
|
75
|
tim@3
|
76 v0.3.1 - 3 Jan. 2008
|
tim@3
|
77 Added OSClient
|
tim@3
|
78 Added OSCRequestHandler, loosely based on the original CallbackManager
|
tim@3
|
79 Added OSCServer
|
tim@3
|
80 Removed original CallbackManager
|
tim@3
|
81 Adapted testing-script (the 'if __name__ == "__main__":' block at the end) to use new Server & Client
|
tim@3
|
82
|
tim@3
|
83 v0.3.2 - 5 Jan. 2008
|
tim@3
|
84 Added 'container-type emulation' methods (getitem(), setitem(), __iter__() & friends) to OSCMessage
|
tim@3
|
85 Added ThreadingOSCServer & ForkingOSCServer
|
tim@3
|
86 - 6 Jan. 2008
|
tim@3
|
87 Added OSCMultiClient
|
tim@3
|
88 Added command-line options to testing-script (try 'python OSC.py --help')
|
tim@3
|
89
|
tim@3
|
90 v0.3.3 - 9 Jan. 2008
|
tim@3
|
91 Added OSC-timetag support to OSCBundle & OSCRequestHandler
|
tim@3
|
92 Added ThreadingOSCRequestHandler
|
tim@3
|
93
|
tim@3
|
94 v0.3.4 - 13 Jan. 2008
|
tim@3
|
95 Added message-filtering to OSCMultiClient
|
tim@3
|
96 Added subscription-handler to OSCServer
|
tim@3
|
97 Added support fon numpy/scipy int & float types. (these get converted to 'standard' 32-bit OSC ints / floats!)
|
tim@3
|
98 Cleaned-up and added more Docstrings
|
tim@3
|
99
|
tim@3
|
100 v0.3.5 - 14 aug. 2008
|
tim@3
|
101 Added OSCServer.reportErr(...) method
|
tim@3
|
102
|
tim@3
|
103 -----------------
|
tim@3
|
104 Original Comments
|
tim@3
|
105 -----------------
|
tim@3
|
106
|
tim@3
|
107 > Open SoundControl for Python
|
tim@3
|
108 > Copyright (C) 2002 Daniel Holth, Clinton McChesney
|
tim@3
|
109 >
|
tim@3
|
110 > This library is free software; you can redistribute it and/or modify it under
|
tim@3
|
111 > the terms of the GNU Lesser General Public License as published by the Free
|
tim@3
|
112 > Software Foundation; either version 2.1 of the License, or (at your option) any
|
tim@3
|
113 > later version.
|
tim@3
|
114 >
|
tim@3
|
115 > This library is distributed in the hope that it will be useful, but WITHOUT ANY
|
tim@3
|
116 > WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
tim@3
|
117 > PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
tim@3
|
118 > details.
|
tim@3
|
119
|
tim@3
|
120 > You should have received a copy of the GNU Lesser General Public License along
|
tim@3
|
121 > with this library; if not, write to the Free Software Foundation, Inc., 59
|
tim@3
|
122 > Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
tim@3
|
123
|
tim@3
|
124 > For questions regarding this module contact Daniel Holth <dholth@stetson.edu>
|
tim@3
|
125 > or visit http://www.stetson.edu/~ProctoLogic/
|
tim@3
|
126
|
tim@3
|
127 > Changelog:
|
tim@3
|
128 > 15 Nov. 2001:
|
tim@3
|
129 > Removed dependency on Python 2.0 features.
|
tim@3
|
130 > - dwh
|
tim@3
|
131 > 13 Feb. 2002:
|
tim@3
|
132 > Added a generic callback handler.
|
tim@3
|
133 > - dwh
|
tim@3
|
134 """
|
tim@3
|
135
|
tim@3
|
136 import math, re, socket, select, string, struct, sys, threading, time, types
|
tim@3
|
137 from SocketServer import UDPServer, DatagramRequestHandler, ForkingMixIn, ThreadingMixIn
|
tim@3
|
138
|
tim@3
|
139 global version
|
tim@3
|
140 version = ("0.3","5b", "$Rev: 5294 $"[6:-2])
|
tim@3
|
141
|
tim@3
|
142 global FloatTypes
|
tim@3
|
143 FloatTypes = [types.FloatType]
|
tim@3
|
144
|
tim@3
|
145 global IntTypes
|
tim@3
|
146 IntTypes = [types.IntType]
|
tim@3
|
147
|
tim@3
|
148 ##
|
tim@3
|
149 # numpy/scipy support:
|
tim@3
|
150 ##
|
tim@3
|
151
|
tim@3
|
152 try:
|
tim@3
|
153 from numpy import typeDict
|
tim@3
|
154
|
tim@3
|
155 for ftype in ['float32', 'float64', 'float128']:
|
tim@3
|
156 try:
|
tim@3
|
157 FloatTypes.append(typeDict[ftype])
|
tim@3
|
158 except KeyError:
|
tim@3
|
159 pass
|
tim@3
|
160
|
tim@3
|
161 for itype in ['int8', 'int16', 'int32', 'int64']:
|
tim@3
|
162 try:
|
tim@3
|
163 IntTypes.append(typeDict[itype])
|
tim@3
|
164 IntTypes.append(typeDict['u' + itype])
|
tim@3
|
165 except KeyError:
|
tim@3
|
166 pass
|
tim@3
|
167
|
tim@3
|
168 # thanks for those...
|
tim@3
|
169 del typeDict, ftype, itype
|
tim@3
|
170
|
tim@3
|
171 except ImportError:
|
tim@3
|
172 pass
|
tim@3
|
173
|
tim@3
|
174 ######
|
tim@3
|
175 #
|
tim@3
|
176 # OSCMessage classes
|
tim@3
|
177 #
|
tim@3
|
178 ######
|
tim@3
|
179
|
tim@3
|
180 class OSCMessage(object):
|
tim@3
|
181 """ Builds typetagged OSC messages.
|
tim@3
|
182
|
tim@3
|
183 OSCMessage objects are container objects for building OSC-messages.
|
tim@3
|
184 On the 'front' end, they behave much like list-objects, and on the 'back' end
|
tim@3
|
185 they generate a binary representation of the message, which can be sent over a network socket.
|
tim@3
|
186 OSC-messages consist of an 'address'-string (not to be confused with a (host, port) IP-address!),
|
tim@3
|
187 followed by a string of 'typetags' associated with the message's arguments (ie. 'payload'),
|
tim@3
|
188 and finally the arguments themselves, encoded in an OSC-specific way.
|
tim@3
|
189
|
tim@3
|
190 On the Python end, OSCMessage are lists of arguments, prepended by the message's address.
|
tim@3
|
191 The message contents can be manipulated much like a list:
|
tim@3
|
192 >>> msg = OSCMessage("/my/osc/address")
|
tim@3
|
193 >>> msg.append('something')
|
tim@3
|
194 >>> msg.insert(0, 'something else')
|
tim@3
|
195 >>> msg[1] = 'entirely'
|
tim@3
|
196 >>> msg.extend([1,2,3.])
|
tim@3
|
197 >>> msg += [4, 5, 6.]
|
tim@3
|
198 >>> del msg[3:6]
|
tim@3
|
199 >>> msg.pop(-2)
|
tim@3
|
200 5
|
tim@3
|
201 >>> print msg
|
tim@3
|
202 /my/osc/address ['something else', 'entirely', 1, 6.0]
|
tim@3
|
203
|
tim@3
|
204 OSCMessages can be concatenated with the + operator. In this case, the resulting OSCMessage
|
tim@3
|
205 inherits its address from the left-hand operand. The right-hand operand's address is ignored.
|
tim@3
|
206 To construct an 'OSC-bundle' from multiple OSCMessage, see OSCBundle!
|
tim@3
|
207
|
tim@3
|
208 Additional methods exist for retreiving typetags or manipulating items as (typetag, value) tuples.
|
tim@3
|
209 """
|
tim@3
|
210 def __init__(self, address=""):
|
tim@3
|
211 """Instantiate a new OSCMessage.
|
tim@3
|
212 The OSC-address can be specified with the 'address' argument
|
tim@3
|
213 """
|
tim@3
|
214 self.clear(address)
|
tim@3
|
215
|
tim@3
|
216 def setAddress(self, address):
|
tim@3
|
217 """Set or change the OSC-address
|
tim@3
|
218 """
|
tim@3
|
219 self.address = address
|
tim@3
|
220
|
tim@3
|
221 def clear(self, address=""):
|
tim@3
|
222 """Clear (or set a new) OSC-address and clear any arguments appended so far
|
tim@3
|
223 """
|
tim@3
|
224 self.address = address
|
tim@3
|
225 self.clearData()
|
tim@3
|
226
|
tim@3
|
227 def clearData(self):
|
tim@3
|
228 """Clear any arguments appended so far
|
tim@3
|
229 """
|
tim@3
|
230 self.typetags = ","
|
tim@3
|
231 self.message = ""
|
tim@3
|
232
|
tim@3
|
233 def append(self, argument, typehint=None):
|
tim@3
|
234 """Appends data to the message, updating the typetags based on
|
tim@3
|
235 the argument's type. If the argument is a blob (counted
|
tim@3
|
236 string) pass in 'b' as typehint.
|
tim@3
|
237 'argument' may also be a list or tuple, in which case its elements
|
tim@3
|
238 will get appended one-by-one, all using the provided typehint
|
tim@3
|
239 """
|
tim@3
|
240 if type(argument) == types.DictType:
|
tim@3
|
241 argument = argument.items()
|
tim@3
|
242 elif isinstance(argument, OSCMessage):
|
tim@3
|
243 raise TypeError("Can only append 'OSCMessage' to 'OSCBundle'")
|
tim@3
|
244
|
tim@3
|
245 if hasattr(argument, '__iter__'):
|
tim@3
|
246 for arg in argument:
|
tim@3
|
247 self.append(arg, typehint)
|
tim@3
|
248
|
tim@3
|
249 return
|
tim@3
|
250
|
tim@3
|
251 if typehint == 'b':
|
tim@3
|
252 binary = OSCBlob(argument)
|
tim@3
|
253 tag = 'b'
|
tim@3
|
254 elif typehint == 't':
|
tim@3
|
255 binary = OSCTimeTag(argument)
|
tim@3
|
256 tag = 't'
|
tim@3
|
257 else:
|
tim@3
|
258 tag, binary = OSCArgument(argument, typehint)
|
tim@3
|
259
|
tim@3
|
260 self.typetags += tag
|
tim@3
|
261 self.message += binary
|
tim@3
|
262
|
tim@3
|
263 def getBinary(self):
|
tim@3
|
264 """Returns the binary representation of the message
|
tim@3
|
265 """
|
tim@3
|
266 binary = OSCString(self.address)
|
tim@3
|
267 binary += OSCString(self.typetags)
|
tim@3
|
268 binary += self.message
|
tim@3
|
269
|
tim@3
|
270 return binary
|
tim@3
|
271
|
tim@3
|
272 def __repr__(self):
|
tim@3
|
273 """Returns a string containing the decode Message
|
tim@3
|
274 """
|
tim@3
|
275 return str(decodeOSC(self.getBinary()))
|
tim@3
|
276
|
tim@3
|
277 def __str__(self):
|
tim@3
|
278 """Returns the Message's address and contents as a string.
|
tim@3
|
279 """
|
tim@3
|
280 return "%s %s" % (self.address, str(self.values()))
|
tim@3
|
281
|
tim@3
|
282 def __len__(self):
|
tim@3
|
283 """Returns the number of arguments appended so far
|
tim@3
|
284 """
|
tim@3
|
285 return (len(self.typetags) - 1)
|
tim@3
|
286
|
tim@3
|
287 def __eq__(self, other):
|
tim@3
|
288 """Return True if two OSCMessages have the same address & content
|
tim@3
|
289 """
|
tim@3
|
290 if not isinstance(other, self.__class__):
|
tim@3
|
291 return False
|
tim@3
|
292
|
tim@3
|
293 return (self.address == other.address) and (self.typetags == other.typetags) and (self.message == other.message)
|
tim@3
|
294
|
tim@3
|
295 def __ne__(self, other):
|
tim@3
|
296 """Return (not self.__eq__(other))
|
tim@3
|
297 """
|
tim@3
|
298 return not self.__eq__(other)
|
tim@3
|
299
|
tim@3
|
300 def __add__(self, values):
|
tim@3
|
301 """Returns a copy of self, with the contents of 'values' appended
|
tim@3
|
302 (see the 'extend()' method, below)
|
tim@3
|
303 """
|
tim@3
|
304 msg = self.copy()
|
tim@3
|
305 msg.extend(values)
|
tim@3
|
306 return msg
|
tim@3
|
307
|
tim@3
|
308 def __iadd__(self, values):
|
tim@3
|
309 """Appends the contents of 'values'
|
tim@3
|
310 (equivalent to 'extend()', below)
|
tim@3
|
311 Returns self
|
tim@3
|
312 """
|
tim@3
|
313 self.extend(values)
|
tim@3
|
314 return self
|
tim@3
|
315
|
tim@3
|
316 def __radd__(self, values):
|
tim@3
|
317 """Appends the contents of this OSCMessage to 'values'
|
tim@3
|
318 Returns the extended 'values' (list or tuple)
|
tim@3
|
319 """
|
tim@3
|
320 out = list(values)
|
tim@3
|
321 out.extend(self.values())
|
tim@3
|
322
|
tim@3
|
323 if type(values) == types.TupleType:
|
tim@3
|
324 return tuple(out)
|
tim@3
|
325
|
tim@3
|
326 return out
|
tim@3
|
327
|
tim@3
|
328 def _reencode(self, items):
|
tim@3
|
329 """Erase & rebuild the OSCMessage contents from the given
|
tim@3
|
330 list of (typehint, value) tuples"""
|
tim@3
|
331 self.clearData()
|
tim@3
|
332 for item in items:
|
tim@3
|
333 self.append(item[1], item[0])
|
tim@3
|
334
|
tim@3
|
335 def values(self):
|
tim@3
|
336 """Returns a list of the arguments appended so far
|
tim@3
|
337 """
|
tim@3
|
338 return decodeOSC(self.getBinary())[2:]
|
tim@3
|
339
|
tim@3
|
340 def tags(self):
|
tim@3
|
341 """Returns a list of typetags of the appended arguments
|
tim@3
|
342 """
|
tim@3
|
343 return list(self.typetags.lstrip(','))
|
tim@3
|
344
|
tim@3
|
345 def items(self):
|
tim@3
|
346 """Returns a list of (typetag, value) tuples for
|
tim@3
|
347 the arguments appended so far
|
tim@3
|
348 """
|
tim@3
|
349 out = []
|
tim@3
|
350 values = self.values()
|
tim@3
|
351 typetags = self.tags()
|
tim@3
|
352 for i in range(len(values)):
|
tim@3
|
353 out.append((typetags[i], values[i]))
|
tim@3
|
354
|
tim@3
|
355 return out
|
tim@3
|
356
|
tim@3
|
357 def __contains__(self, val):
|
tim@3
|
358 """Test if the given value appears in the OSCMessage's arguments
|
tim@3
|
359 """
|
tim@3
|
360 return (val in self.values())
|
tim@3
|
361
|
tim@3
|
362 def __getitem__(self, i):
|
tim@3
|
363 """Returns the indicated argument (or slice)
|
tim@3
|
364 """
|
tim@3
|
365 return self.values()[i]
|
tim@3
|
366
|
tim@3
|
367 def __delitem__(self, i):
|
tim@3
|
368 """Removes the indicated argument (or slice)
|
tim@3
|
369 """
|
tim@3
|
370 items = self.items()
|
tim@3
|
371 del items[i]
|
tim@3
|
372
|
tim@3
|
373 self._reencode(items)
|
tim@3
|
374
|
tim@3
|
375 def _buildItemList(self, values, typehint=None):
|
tim@3
|
376 if isinstance(values, OSCMessage):
|
tim@3
|
377 items = values.items()
|
tim@3
|
378 elif type(values) == types.ListType:
|
tim@3
|
379 items = []
|
tim@3
|
380 for val in values:
|
tim@3
|
381 if type(val) == types.TupleType:
|
tim@3
|
382 items.append(val[:2])
|
tim@3
|
383 else:
|
tim@3
|
384 items.append((typehint, val))
|
tim@3
|
385 elif type(values) == types.TupleType:
|
tim@3
|
386 items = [values[:2]]
|
tim@3
|
387 else:
|
tim@3
|
388 items = [(typehint, values)]
|
tim@3
|
389
|
tim@3
|
390 return items
|
tim@3
|
391
|
tim@3
|
392 def __setitem__(self, i, val):
|
tim@3
|
393 """Set indicatated argument (or slice) to a new value.
|
tim@3
|
394 'val' can be a single int/float/string, or a (typehint, value) tuple.
|
tim@3
|
395 Or, if 'i' is a slice, a list of these or another OSCMessage.
|
tim@3
|
396 """
|
tim@3
|
397 items = self.items()
|
tim@3
|
398
|
tim@3
|
399 new_items = self._buildItemList(val)
|
tim@3
|
400
|
tim@3
|
401 if type(i) != types.SliceType:
|
tim@3
|
402 if len(new_items) != 1:
|
tim@3
|
403 raise TypeError("single-item assignment expects a single value or a (typetag, value) tuple")
|
tim@3
|
404
|
tim@3
|
405 new_items = new_items[0]
|
tim@3
|
406
|
tim@3
|
407 # finally...
|
tim@3
|
408 items[i] = new_items
|
tim@3
|
409
|
tim@3
|
410 self._reencode(items)
|
tim@3
|
411
|
tim@3
|
412 def setItem(self, i, val, typehint=None):
|
tim@3
|
413 """Set indicated argument to a new value (with typehint)
|
tim@3
|
414 """
|
tim@3
|
415 items = self.items()
|
tim@3
|
416
|
tim@3
|
417 items[i] = (typehint, val)
|
tim@3
|
418
|
tim@3
|
419 self._reencode(items)
|
tim@3
|
420
|
tim@3
|
421 def copy(self):
|
tim@3
|
422 """Returns a deep copy of this OSCMessage
|
tim@3
|
423 """
|
tim@3
|
424 msg = self.__class__(self.address)
|
tim@3
|
425 msg.typetags = self.typetags
|
tim@3
|
426 msg.message = self.message
|
tim@3
|
427 return msg
|
tim@3
|
428
|
tim@3
|
429 def count(self, val):
|
tim@3
|
430 """Returns the number of times the given value occurs in the OSCMessage's arguments
|
tim@3
|
431 """
|
tim@3
|
432 return self.values().count(val)
|
tim@3
|
433
|
tim@3
|
434 def index(self, val):
|
tim@3
|
435 """Returns the index of the first occurence of the given value in the OSCMessage's arguments.
|
tim@3
|
436 Raises ValueError if val isn't found
|
tim@3
|
437 """
|
tim@3
|
438 return self.values().index(val)
|
tim@3
|
439
|
tim@3
|
440 def extend(self, values):
|
tim@3
|
441 """Append the contents of 'values' to this OSCMessage.
|
tim@3
|
442 'values' can be another OSCMessage, or a list/tuple of ints/floats/strings
|
tim@3
|
443 """
|
tim@3
|
444 items = self.items() + self._buildItemList(values)
|
tim@3
|
445
|
tim@3
|
446 self._reencode(items)
|
tim@3
|
447
|
tim@3
|
448 def insert(self, i, val, typehint = None):
|
tim@3
|
449 """Insert given value (with optional typehint) into the OSCMessage
|
tim@3
|
450 at the given index.
|
tim@3
|
451 """
|
tim@3
|
452 items = self.items()
|
tim@3
|
453
|
tim@3
|
454 for item in reversed(self._buildItemList(val)):
|
tim@3
|
455 items.insert(i, item)
|
tim@3
|
456
|
tim@3
|
457 self._reencode(items)
|
tim@3
|
458
|
tim@3
|
459 def popitem(self, i):
|
tim@3
|
460 """Delete the indicated argument from the OSCMessage, and return it
|
tim@3
|
461 as a (typetag, value) tuple.
|
tim@3
|
462 """
|
tim@3
|
463 items = self.items()
|
tim@3
|
464
|
tim@3
|
465 item = items.pop(i)
|
tim@3
|
466
|
tim@3
|
467 self._reencode(items)
|
tim@3
|
468
|
tim@3
|
469 return item
|
tim@3
|
470
|
tim@3
|
471 def pop(self, i):
|
tim@3
|
472 """Delete the indicated argument from the OSCMessage, and return it.
|
tim@3
|
473 """
|
tim@3
|
474 return self.popitem(i)[1]
|
tim@3
|
475
|
tim@3
|
476 def reverse(self):
|
tim@3
|
477 """Reverses the arguments of the OSCMessage (in place)
|
tim@3
|
478 """
|
tim@3
|
479 items = self.items()
|
tim@3
|
480
|
tim@3
|
481 items.reverse()
|
tim@3
|
482
|
tim@3
|
483 self._reencode(items)
|
tim@3
|
484
|
tim@3
|
485 def remove(self, val):
|
tim@3
|
486 """Removes the first argument with the given value from the OSCMessage.
|
tim@3
|
487 Raises ValueError if val isn't found.
|
tim@3
|
488 """
|
tim@3
|
489 items = self.items()
|
tim@3
|
490
|
tim@3
|
491 # this is not very efficient...
|
tim@3
|
492 i = 0
|
tim@3
|
493 for (t, v) in items:
|
tim@3
|
494 if (v == val):
|
tim@3
|
495 break
|
tim@3
|
496 i += 1
|
tim@3
|
497 else:
|
tim@3
|
498 raise ValueError("'%s' not in OSCMessage" % str(m))
|
tim@3
|
499 # but more efficient than first calling self.values().index(val),
|
tim@3
|
500 # then calling self.items(), which would in turn call self.values() again...
|
tim@3
|
501
|
tim@3
|
502 del items[i]
|
tim@3
|
503
|
tim@3
|
504 self._reencode(items)
|
tim@3
|
505
|
tim@3
|
506 def __iter__(self):
|
tim@3
|
507 """Returns an iterator of the OSCMessage's arguments
|
tim@3
|
508 """
|
tim@3
|
509 return iter(self.values())
|
tim@3
|
510
|
tim@3
|
511 def __reversed__(self):
|
tim@3
|
512 """Returns a reverse iterator of the OSCMessage's arguments
|
tim@3
|
513 """
|
tim@3
|
514 return reversed(self.values())
|
tim@3
|
515
|
tim@3
|
516 def itervalues(self):
|
tim@3
|
517 """Returns an iterator of the OSCMessage's arguments
|
tim@3
|
518 """
|
tim@3
|
519 return iter(self.values())
|
tim@3
|
520
|
tim@3
|
521 def iteritems(self):
|
tim@3
|
522 """Returns an iterator of the OSCMessage's arguments as
|
tim@3
|
523 (typetag, value) tuples
|
tim@3
|
524 """
|
tim@3
|
525 return iter(self.items())
|
tim@3
|
526
|
tim@3
|
527 def itertags(self):
|
tim@3
|
528 """Returns an iterator of the OSCMessage's arguments' typetags
|
tim@3
|
529 """
|
tim@3
|
530 return iter(self.tags())
|
tim@3
|
531
|
tim@3
|
532 class OSCBundle(OSCMessage):
|
tim@3
|
533 """Builds a 'bundle' of OSC messages.
|
tim@3
|
534
|
tim@3
|
535 OSCBundle objects are container objects for building OSC-bundles of OSC-messages.
|
tim@3
|
536 An OSC-bundle is a special kind of OSC-message which contains a list of OSC-messages
|
tim@3
|
537 (And yes, OSC-bundles may contain other OSC-bundles...)
|
tim@3
|
538
|
tim@3
|
539 OSCBundle objects behave much the same as OSCMessage objects, with these exceptions:
|
tim@3
|
540 - if an item or items to be appended or inserted are not OSCMessage objects,
|
tim@3
|
541 OSCMessage objectss are created to encapsulate the item(s)
|
tim@3
|
542 - an OSC-bundle does not have an address of its own, only the contained OSC-messages do.
|
tim@3
|
543 The OSCBundle's 'address' is inherited by any OSCMessage the OSCBundle object creates.
|
tim@3
|
544 - OSC-bundles have a timetag to tell the receiver when the bundle should be processed.
|
tim@3
|
545 The default timetag value (0) means 'immediately'
|
tim@3
|
546 """
|
tim@3
|
547 def __init__(self, address="", time=0):
|
tim@3
|
548 """Instantiate a new OSCBundle.
|
tim@3
|
549 The default OSC-address for newly created OSCMessages
|
tim@3
|
550 can be specified with the 'address' argument
|
tim@3
|
551 The bundle's timetag can be set with the 'time' argument
|
tim@3
|
552 """
|
tim@3
|
553 super(OSCBundle, self).__init__(address)
|
tim@3
|
554 self.timetag = time
|
tim@3
|
555
|
tim@3
|
556 def __str__(self):
|
tim@3
|
557 """Returns the Bundle's contents (and timetag, if nonzero) as a string.
|
tim@3
|
558 """
|
tim@3
|
559 if (self.timetag > 0.):
|
tim@3
|
560 out = "#bundle (%s) [" % self.getTimeTagStr()
|
tim@3
|
561 else:
|
tim@3
|
562 out = "#bundle ["
|
tim@3
|
563
|
tim@3
|
564 if self.__len__():
|
tim@3
|
565 for val in self.values():
|
tim@3
|
566 out += "%s, " % str(val)
|
tim@3
|
567 out = out[:-2] # strip trailing space and comma
|
tim@3
|
568
|
tim@3
|
569 return out + "]"
|
tim@3
|
570
|
tim@3
|
571 def setTimeTag(self, time):
|
tim@3
|
572 """Set or change the OSCBundle's TimeTag
|
tim@3
|
573 In 'Python Time', that's floating seconds since the Epoch
|
tim@3
|
574 """
|
tim@3
|
575 if time >= 0:
|
tim@3
|
576 self.timetag = time
|
tim@3
|
577
|
tim@3
|
578 def getTimeTagStr(self):
|
tim@3
|
579 """Return the TimeTag as a human-readable string
|
tim@3
|
580 """
|
tim@3
|
581 fract, secs = math.modf(self.timetag)
|
tim@3
|
582 out = time.ctime(secs)[11:19]
|
tim@3
|
583 out += ("%.3f" % fract)[1:]
|
tim@3
|
584
|
tim@3
|
585 return out
|
tim@3
|
586
|
tim@3
|
587 def append(self, argument, typehint = None):
|
tim@3
|
588 """Appends data to the bundle, creating an OSCMessage to encapsulate
|
tim@3
|
589 the provided argument unless this is already an OSCMessage.
|
tim@3
|
590 Any newly created OSCMessage inherits the OSCBundle's address at the time of creation.
|
tim@3
|
591 If 'argument' is an iterable, its elements will be encapsuated by a single OSCMessage.
|
tim@3
|
592 Finally, 'argument' can be (or contain) a dict, which will be 'converted' to an OSCMessage;
|
tim@3
|
593 - if 'addr' appears in the dict, its value overrides the OSCBundle's address
|
tim@3
|
594 - if 'args' appears in the dict, its value(s) become the OSCMessage's arguments
|
tim@3
|
595 """
|
tim@3
|
596 if isinstance(argument, OSCMessage):
|
tim@3
|
597 binary = OSCBlob(argument.getBinary())
|
tim@3
|
598 else:
|
tim@3
|
599 msg = OSCMessage(self.address)
|
tim@3
|
600 if type(argument) == types.DictType:
|
tim@3
|
601 if 'addr' in argument:
|
tim@3
|
602 msg.setAddress(argument['addr'])
|
tim@3
|
603 if 'args' in argument:
|
tim@3
|
604 msg.append(argument['args'], typehint)
|
tim@3
|
605 else:
|
tim@3
|
606 msg.append(argument, typehint)
|
tim@3
|
607
|
tim@3
|
608 binary = OSCBlob(msg.getBinary())
|
tim@3
|
609
|
tim@3
|
610 self.message += binary
|
tim@3
|
611 self.typetags += 'b'
|
tim@3
|
612
|
tim@3
|
613 def getBinary(self):
|
tim@3
|
614 """Returns the binary representation of the message
|
tim@3
|
615 """
|
tim@3
|
616 binary = OSCString("#bundle")
|
tim@3
|
617 binary += OSCTimeTag(self.timetag)
|
tim@3
|
618 binary += self.message
|
tim@3
|
619
|
tim@3
|
620 return binary
|
tim@3
|
621
|
tim@3
|
622 def _reencapsulate(self, decoded):
|
tim@3
|
623 if decoded[0] == "#bundle":
|
tim@3
|
624 msg = OSCBundle()
|
tim@3
|
625 msg.setTimeTag(decoded[1])
|
tim@3
|
626 for submsg in decoded[2:]:
|
tim@3
|
627 msg.append(self._reencapsulate(submsg))
|
tim@3
|
628
|
tim@3
|
629 else:
|
tim@3
|
630 msg = OSCMessage(decoded[0])
|
tim@3
|
631 tags = decoded[1].lstrip(',')
|
tim@3
|
632 for i in range(len(tags)):
|
tim@3
|
633 msg.append(decoded[2+i], tags[i])
|
tim@3
|
634
|
tim@3
|
635 return msg
|
tim@3
|
636
|
tim@3
|
637 def values(self):
|
tim@3
|
638 """Returns a list of the OSCMessages appended so far
|
tim@3
|
639 """
|
tim@3
|
640 out = []
|
tim@3
|
641 for decoded in decodeOSC(self.getBinary())[2:]:
|
tim@3
|
642 out.append(self._reencapsulate(decoded))
|
tim@3
|
643
|
tim@3
|
644 return out
|
tim@3
|
645
|
tim@3
|
646 def __eq__(self, other):
|
tim@3
|
647 """Return True if two OSCBundles have the same timetag & content
|
tim@3
|
648 """
|
tim@3
|
649 if not isinstance(other, self.__class__):
|
tim@3
|
650 return False
|
tim@3
|
651
|
tim@3
|
652 return (self.timetag == other.timetag) and (self.typetags == other.typetags) and (self.message == other.message)
|
tim@3
|
653
|
tim@3
|
654 def copy(self):
|
tim@3
|
655 """Returns a deep copy of this OSCBundle
|
tim@3
|
656 """
|
tim@3
|
657 copy = super(OSCBundle, self).copy()
|
tim@3
|
658 copy.timetag = self.timetag
|
tim@3
|
659 return copy
|
tim@3
|
660
|
tim@3
|
661 ######
|
tim@3
|
662 #
|
tim@3
|
663 # OSCMessage encoding functions
|
tim@3
|
664 #
|
tim@3
|
665 ######
|
tim@3
|
666
|
tim@3
|
667 def OSCString(next):
|
tim@3
|
668 """Convert a string into a zero-padded OSC String.
|
tim@3
|
669 The length of the resulting string is always a multiple of 4 bytes.
|
tim@3
|
670 The string ends with 1 to 4 zero-bytes ('\x00')
|
tim@3
|
671 """
|
tim@3
|
672
|
tim@3
|
673 OSCstringLength = math.ceil((len(next)+1) / 4.0) * 4
|
tim@3
|
674 return struct.pack(">%ds" % (OSCstringLength), str(next))
|
tim@3
|
675
|
tim@3
|
676 def OSCBlob(next):
|
tim@3
|
677 """Convert a string into an OSC Blob.
|
tim@3
|
678 An OSC-Blob is a binary encoded block of data, prepended by a 'size' (int32).
|
tim@3
|
679 The size is always a mutiple of 4 bytes.
|
tim@3
|
680 The blob ends with 0 to 3 zero-bytes ('\x00')
|
tim@3
|
681 """
|
tim@3
|
682
|
tim@3
|
683 if type(next) in types.StringTypes:
|
tim@3
|
684 OSCblobLength = math.ceil((len(next)) / 4.0) * 4
|
tim@3
|
685 binary = struct.pack(">i%ds" % (OSCblobLength), OSCblobLength, next)
|
tim@3
|
686 else:
|
tim@3
|
687 binary = ""
|
tim@3
|
688
|
tim@3
|
689 return binary
|
tim@3
|
690
|
tim@3
|
691 def OSCArgument(next, typehint=None):
|
tim@3
|
692 """ Convert some Python types to their
|
tim@3
|
693 OSC binary representations, returning a
|
tim@3
|
694 (typetag, data) tuple.
|
tim@3
|
695 """
|
tim@3
|
696 if not typehint:
|
tim@3
|
697 if type(next) in FloatTypes:
|
tim@3
|
698 binary = struct.pack(">f", float(next))
|
tim@3
|
699 tag = 'f'
|
tim@3
|
700 elif type(next) in IntTypes:
|
tim@3
|
701 binary = struct.pack(">i", int(next))
|
tim@3
|
702 tag = 'i'
|
tim@3
|
703 else:
|
tim@3
|
704 binary = OSCString(next)
|
tim@3
|
705 tag = 's'
|
tim@3
|
706
|
tim@3
|
707 elif typehint == 'f':
|
tim@3
|
708 try:
|
tim@3
|
709 binary = struct.pack(">f", float(next))
|
tim@3
|
710 tag = 'f'
|
tim@3
|
711 except ValueError:
|
tim@3
|
712 binary = OSCString(next)
|
tim@3
|
713 tag = 's'
|
tim@3
|
714 elif typehint == 'i':
|
tim@3
|
715 try:
|
tim@3
|
716 binary = struct.pack(">i", int(next))
|
tim@3
|
717 tag = 'i'
|
tim@3
|
718 except ValueError:
|
tim@3
|
719 binary = OSCString(next)
|
tim@3
|
720 tag = 's'
|
tim@3
|
721 else:
|
tim@3
|
722 binary = OSCString(next)
|
tim@3
|
723 tag = 's'
|
tim@3
|
724
|
tim@3
|
725 return (tag, binary)
|
tim@3
|
726
|
tim@3
|
727 def OSCTimeTag(time):
|
tim@3
|
728 """Convert a time in floating seconds to its
|
tim@3
|
729 OSC binary representation
|
tim@3
|
730 """
|
tim@3
|
731 if time > 0:
|
tim@3
|
732 fract, secs = math.modf(time)
|
tim@3
|
733 binary = struct.pack('>ll', long(secs), long(fract * 1e9))
|
tim@3
|
734 else:
|
tim@3
|
735 binary = struct.pack('>ll', 0L, 1L)
|
tim@3
|
736
|
tim@3
|
737 return binary
|
tim@3
|
738
|
tim@3
|
739 ######
|
tim@3
|
740 #
|
tim@3
|
741 # OSCMessage decoding functions
|
tim@3
|
742 #
|
tim@3
|
743 ######
|
tim@3
|
744
|
tim@3
|
745 def _readString(data):
|
tim@3
|
746 """Reads the next (null-terminated) block of data
|
tim@3
|
747 """
|
tim@3
|
748 length = string.find(data,"\0")
|
tim@3
|
749 nextData = int(math.ceil((length+1) / 4.0) * 4)
|
tim@3
|
750 return (data[0:length], data[nextData:])
|
tim@3
|
751
|
tim@3
|
752 def _readBlob(data):
|
tim@3
|
753 """Reads the next (numbered) block of data
|
tim@3
|
754 """
|
tim@3
|
755
|
tim@3
|
756 length = struct.unpack(">i", data[0:4])[0]
|
tim@3
|
757 nextData = int(math.ceil((length) / 4.0) * 4) + 4
|
tim@3
|
758 return (data[4:length+4], data[nextData:])
|
tim@3
|
759
|
tim@3
|
760 def _readInt(data):
|
tim@3
|
761 """Tries to interpret the next 4 bytes of the data
|
tim@3
|
762 as a 32-bit integer. """
|
tim@3
|
763
|
tim@3
|
764 if(len(data)<4):
|
tim@3
|
765 print "Error: too few bytes for int", data, len(data)
|
tim@3
|
766 rest = data
|
tim@3
|
767 integer = 0
|
tim@3
|
768 else:
|
tim@3
|
769 integer = struct.unpack(">i", data[0:4])[0]
|
tim@3
|
770 rest = data[4:]
|
tim@3
|
771
|
tim@3
|
772 return (integer, rest)
|
tim@3
|
773
|
tim@3
|
774 def _readLong(data):
|
tim@3
|
775 """Tries to interpret the next 8 bytes of the data
|
tim@3
|
776 as a 64-bit signed integer.
|
tim@3
|
777 """
|
tim@3
|
778
|
tim@3
|
779 high, low = struct.unpack(">ll", data[0:8])
|
tim@3
|
780 big = (long(high) << 32) + low
|
tim@3
|
781 rest = data[8:]
|
tim@3
|
782 return (big, rest)
|
tim@3
|
783
|
tim@3
|
784 def _readTimeTag(data):
|
tim@3
|
785 """Tries to interpret the next 8 bytes of the data
|
tim@3
|
786 as a TimeTag.
|
tim@3
|
787 """
|
tim@3
|
788 high, low = struct.unpack(">ll", data[0:8])
|
tim@3
|
789 if (high == 0) and (low <= 1):
|
tim@3
|
790 time = 0.0
|
tim@3
|
791 else:
|
tim@3
|
792 time = int(high) + float(low / 1e9)
|
tim@3
|
793 rest = data[8:]
|
tim@3
|
794 return (time, rest)
|
tim@3
|
795
|
tim@3
|
796 def _readFloat(data):
|
tim@3
|
797 """Tries to interpret the next 4 bytes of the data
|
tim@3
|
798 as a 32-bit float.
|
tim@3
|
799 """
|
tim@3
|
800
|
tim@3
|
801 if(len(data)<4):
|
tim@3
|
802 print "Error: too few bytes for float", data, len(data)
|
tim@3
|
803 rest = data
|
tim@3
|
804 float = 0
|
tim@3
|
805 else:
|
tim@3
|
806 float = struct.unpack(">f", data[0:4])[0]
|
tim@3
|
807 rest = data[4:]
|
tim@3
|
808
|
tim@3
|
809 return (float, rest)
|
tim@3
|
810
|
tim@3
|
811 def decodeOSC(data):
|
tim@3
|
812 """Converts a binary OSC message to a Python list.
|
tim@3
|
813 """
|
tim@3
|
814 table = {"i":_readInt, "f":_readFloat, "s":_readString, "b":_readBlob}
|
tim@3
|
815 decoded = []
|
tim@3
|
816 address, rest = _readString(data)
|
tim@3
|
817 if address.startswith(","):
|
tim@3
|
818 typetags = address
|
tim@3
|
819 address = ""
|
tim@3
|
820 else:
|
tim@3
|
821 typetags = ""
|
tim@3
|
822
|
tim@3
|
823 if address == "#bundle":
|
tim@3
|
824 time, rest = _readTimeTag(rest)
|
tim@3
|
825 decoded.append(address)
|
tim@3
|
826 decoded.append(time)
|
tim@3
|
827 while len(rest)>0:
|
tim@3
|
828 length, rest = _readInt(rest)
|
tim@3
|
829 decoded.append(decodeOSC(rest[:length]))
|
tim@3
|
830 rest = rest[length:]
|
tim@3
|
831
|
tim@3
|
832 elif len(rest)>0:
|
tim@3
|
833 if not len(typetags):
|
tim@3
|
834 typetags, rest = _readString(rest)
|
tim@3
|
835 decoded.append(address)
|
tim@3
|
836 decoded.append(typetags)
|
tim@3
|
837 if typetags.startswith(","):
|
tim@3
|
838 for tag in typetags[1:]:
|
tim@3
|
839 value, rest = table[tag](rest)
|
tim@3
|
840 decoded.append(value)
|
tim@3
|
841 else:
|
tim@3
|
842 raise OSCError("OSCMessage's typetag-string lacks the magic ','")
|
tim@3
|
843
|
tim@3
|
844 return decoded
|
tim@3
|
845
|
tim@3
|
846 ######
|
tim@3
|
847 #
|
tim@3
|
848 # Utility functions
|
tim@3
|
849 #
|
tim@3
|
850 ######
|
tim@3
|
851
|
tim@3
|
852 def hexDump(bytes):
|
tim@3
|
853 """ Useful utility; prints the string in hexadecimal.
|
tim@3
|
854 """
|
tim@3
|
855 print "byte 0 1 2 3 4 5 6 7 8 9 A B C D E F"
|
tim@3
|
856
|
tim@3
|
857 num = len(bytes)
|
tim@3
|
858 for i in range(num):
|
tim@3
|
859 if (i) % 16 == 0:
|
tim@3
|
860 line = "%02X0 : " % (i/16)
|
tim@3
|
861 line += "%02X " % ord(bytes[i])
|
tim@3
|
862 if (i+1) % 16 == 0:
|
tim@3
|
863 print "%s: %s" % (line, repr(bytes[i-15:i+1]))
|
tim@3
|
864 line = ""
|
tim@3
|
865
|
tim@3
|
866 bytes_left = num % 16
|
tim@3
|
867 if bytes_left:
|
tim@3
|
868 print "%s: %s" % (line.ljust(54), repr(bytes[-bytes_left:]))
|
tim@3
|
869
|
tim@3
|
870 def getUrlStr(*args):
|
tim@3
|
871 """Convert provided arguments to a string in 'host:port/prefix' format
|
tim@3
|
872 Args can be:
|
tim@3
|
873 - (host, port)
|
tim@3
|
874 - (host, port), prefix
|
tim@3
|
875 - host, port
|
tim@3
|
876 - host, port, prefix
|
tim@3
|
877 """
|
tim@3
|
878 if not len(args):
|
tim@3
|
879 return ""
|
tim@3
|
880
|
tim@3
|
881 if type(args[0]) == types.TupleType:
|
tim@3
|
882 host = args[0][0]
|
tim@3
|
883 port = args[0][1]
|
tim@3
|
884 args = args[1:]
|
tim@3
|
885 else:
|
tim@3
|
886 host = args[0]
|
tim@3
|
887 port = args[1]
|
tim@3
|
888 args = args[2:]
|
tim@3
|
889
|
tim@3
|
890 if len(args):
|
tim@3
|
891 prefix = args[0]
|
tim@3
|
892 else:
|
tim@3
|
893 prefix = ""
|
tim@3
|
894
|
tim@3
|
895 if len(host) and (host != '0.0.0.0'):
|
tim@3
|
896 try:
|
tim@3
|
897 (host, _, _) = socket.gethostbyaddr(host)
|
tim@3
|
898 except socket.error:
|
tim@3
|
899 pass
|
tim@3
|
900 else:
|
tim@3
|
901 host = 'localhost'
|
tim@3
|
902
|
tim@3
|
903 if type(port) == types.IntType:
|
tim@3
|
904 return "%s:%d%s" % (host, port, prefix)
|
tim@3
|
905 else:
|
tim@3
|
906 return host + prefix
|
tim@3
|
907
|
tim@3
|
908 def parseUrlStr(url):
|
tim@3
|
909 """Convert provided string in 'host:port/prefix' format to it's components
|
tim@3
|
910 Returns ((host, port), prefix)
|
tim@3
|
911 """
|
tim@3
|
912 if not (type(url) in types.StringTypes and len(url)):
|
tim@3
|
913 return (None, '')
|
tim@3
|
914
|
tim@3
|
915 i = url.find("://")
|
tim@3
|
916 if i > -1:
|
tim@3
|
917 url = url[i+3:]
|
tim@3
|
918
|
tim@3
|
919 i = url.find(':')
|
tim@3
|
920 if i > -1:
|
tim@3
|
921 host = url[:i].strip()
|
tim@3
|
922 tail = url[i+1:].strip()
|
tim@3
|
923 else:
|
tim@3
|
924 host = ''
|
tim@3
|
925 tail = url
|
tim@3
|
926
|
tim@3
|
927 for i in range(len(tail)):
|
tim@3
|
928 if not tail[i].isdigit():
|
tim@3
|
929 break
|
tim@3
|
930 else:
|
tim@3
|
931 i += 1
|
tim@3
|
932
|
tim@3
|
933 portstr = tail[:i].strip()
|
tim@3
|
934 tail = tail[i:].strip()
|
tim@3
|
935
|
tim@3
|
936 found = len(tail)
|
tim@3
|
937 for c in ('/', '+', '-', '*'):
|
tim@3
|
938 i = tail.find(c)
|
tim@3
|
939 if (i > -1) and (i < found):
|
tim@3
|
940 found = i
|
tim@3
|
941
|
tim@3
|
942 head = tail[:found].strip()
|
tim@3
|
943 prefix = tail[found:].strip()
|
tim@3
|
944
|
tim@3
|
945 prefix = prefix.strip('/')
|
tim@3
|
946 if len(prefix) and prefix[0] not in ('+', '-', '*'):
|
tim@3
|
947 prefix = '/' + prefix
|
tim@3
|
948
|
tim@3
|
949 if len(head) and not len(host):
|
tim@3
|
950 host = head
|
tim@3
|
951
|
tim@3
|
952 if len(host):
|
tim@3
|
953 try:
|
tim@3
|
954 host = socket.gethostbyname(host)
|
tim@3
|
955 except socket.error:
|
tim@3
|
956 pass
|
tim@3
|
957
|
tim@3
|
958 try:
|
tim@3
|
959 port = int(portstr)
|
tim@3
|
960 except ValueError:
|
tim@3
|
961 port = None
|
tim@3
|
962
|
tim@3
|
963 return ((host, port), prefix)
|
tim@3
|
964
|
tim@3
|
965 ######
|
tim@3
|
966 #
|
tim@3
|
967 # OSCClient class
|
tim@3
|
968 #
|
tim@3
|
969 ######
|
tim@3
|
970
|
tim@3
|
971 class OSCClient(object):
|
tim@3
|
972 """Simple OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket
|
tim@3
|
973 """
|
tim@3
|
974 # set outgoing socket buffer size
|
tim@3
|
975 sndbuf_size = 4096 * 8
|
tim@3
|
976
|
tim@3
|
977 def __init__(self, server=None):
|
tim@3
|
978 """Construct an OSC Client.
|
tim@3
|
979 When the 'address' argument is given this client is connected to a specific remote server.
|
tim@3
|
980 - address ((host, port) tuple): the address of the remote server to send all messages to
|
tim@3
|
981 Otherwise it acts as a generic client:
|
tim@3
|
982 If address == 'None', the client doesn't connect to a specific remote server,
|
tim@3
|
983 and the remote address must be supplied when calling sendto()
|
tim@3
|
984 - server: Local OSCServer-instance this client will use the socket of for transmissions.
|
tim@3
|
985 If none is supplied, a socket will be created.
|
tim@3
|
986 """
|
tim@3
|
987 self.socket = None
|
tim@3
|
988
|
tim@3
|
989 if server == None:
|
tim@3
|
990 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
tim@3
|
991 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size)
|
tim@3
|
992 self._fd = self.socket.fileno()
|
tim@3
|
993
|
tim@3
|
994 self.server = None
|
tim@3
|
995 else:
|
tim@3
|
996 self.setServer(server)
|
tim@3
|
997
|
tim@3
|
998 self.client_address = None
|
tim@3
|
999
|
tim@3
|
1000 def setServer(self, server):
|
tim@3
|
1001 """Associate this Client with given server.
|
tim@3
|
1002 The Client will send from the Server's socket.
|
tim@3
|
1003 The Server will use this Client instance to send replies.
|
tim@3
|
1004 """
|
tim@3
|
1005 if not isinstance(server, OSCServer):
|
tim@3
|
1006 raise ValueError("'server' argument is not a valid OSCServer object")
|
tim@3
|
1007
|
tim@3
|
1008 if self.socket != None:
|
tim@3
|
1009 self.close()
|
tim@3
|
1010
|
tim@3
|
1011 self.socket = server.socket.dup()
|
tim@3
|
1012 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size)
|
tim@3
|
1013 self._fd = self.socket.fileno()
|
tim@3
|
1014
|
tim@3
|
1015 self.server = server
|
tim@3
|
1016
|
tim@3
|
1017 if self.server.client != None:
|
tim@3
|
1018 self.server.client.close()
|
tim@3
|
1019
|
tim@3
|
1020 self.server.client = self
|
tim@3
|
1021
|
tim@3
|
1022 def close(self):
|
tim@3
|
1023 """Disconnect & close the Client's socket
|
tim@3
|
1024 """
|
tim@3
|
1025 if self.socket != None:
|
tim@3
|
1026 self.socket.close()
|
tim@3
|
1027 self.socket = None
|
tim@3
|
1028
|
tim@3
|
1029 def __str__(self):
|
tim@3
|
1030 """Returns a string containing this Client's Class-name, software-version
|
tim@3
|
1031 and the remote-address it is connected to (if any)
|
tim@3
|
1032 """
|
tim@3
|
1033 out = self.__class__.__name__
|
tim@3
|
1034 out += " v%s.%s-%s" % version
|
tim@3
|
1035 addr = self.address()
|
tim@3
|
1036 if addr:
|
tim@3
|
1037 out += " connected to osc://%s" % getUrlStr(addr)
|
tim@3
|
1038 else:
|
tim@3
|
1039 out += " (unconnected)"
|
tim@3
|
1040
|
tim@3
|
1041 return out
|
tim@3
|
1042
|
tim@3
|
1043 def __eq__(self, other):
|
tim@3
|
1044 """Compare function.
|
tim@3
|
1045 """
|
tim@3
|
1046 if not isinstance(other, self.__class__):
|
tim@3
|
1047 return False
|
tim@3
|
1048
|
tim@3
|
1049 isequal = cmp(self.socket._sock, other.socket._sock)
|
tim@3
|
1050 if isequal and self.server and other.server:
|
tim@3
|
1051 return cmp(self.server, other.server)
|
tim@3
|
1052
|
tim@3
|
1053 return isequal
|
tim@3
|
1054
|
tim@3
|
1055 def __ne__(self, other):
|
tim@3
|
1056 """Compare function.
|
tim@3
|
1057 """
|
tim@3
|
1058 return not self.__eq__(other)
|
tim@3
|
1059
|
tim@3
|
1060 def address(self):
|
tim@3
|
1061 """Returns a (host,port) tuple of the remote server this client is
|
tim@3
|
1062 connected to or None if not connected to any server.
|
tim@3
|
1063 """
|
tim@3
|
1064 try:
|
tim@3
|
1065 return self.socket.getpeername()
|
tim@3
|
1066 except socket.error:
|
tim@3
|
1067 return None
|
tim@3
|
1068
|
tim@3
|
1069 def connect(self, address):
|
tim@3
|
1070 """Bind to a specific OSC server:
|
tim@3
|
1071 the 'address' argument is a (host, port) tuple
|
tim@3
|
1072 - host: hostname of the remote OSC server,
|
tim@3
|
1073 - port: UDP-port the remote OSC server listens to.
|
tim@3
|
1074 """
|
tim@3
|
1075 try:
|
tim@3
|
1076 self.socket.connect(address)
|
tim@3
|
1077 self.client_address = address
|
tim@3
|
1078 except socket.error, e:
|
tim@3
|
1079 self.client_address = None
|
tim@3
|
1080 raise OSCClientError("SocketError: %s" % str(e))
|
tim@3
|
1081
|
tim@3
|
1082 if self.server != None:
|
tim@3
|
1083 self.server.return_port = address[1]
|
tim@3
|
1084
|
tim@3
|
1085 def sendto(self, msg, address, timeout=None):
|
tim@3
|
1086 """Send the given OSCMessage to the specified address.
|
tim@3
|
1087 - msg: OSCMessage (or OSCBundle) to be sent
|
tim@3
|
1088 - address: (host, port) tuple specifing remote server to send the message to
|
tim@3
|
1089 - timeout: A timeout value for attempting to send. If timeout == None,
|
tim@3
|
1090 this call blocks until socket is available for writing.
|
tim@3
|
1091 Raises OSCClientError when timing out while waiting for the socket.
|
tim@3
|
1092 """
|
tim@3
|
1093 if not isinstance(msg, OSCMessage):
|
tim@3
|
1094 raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object")
|
tim@3
|
1095
|
tim@3
|
1096 ret = select.select([],[self._fd], [], timeout)
|
tim@3
|
1097 try:
|
tim@3
|
1098 ret[1].index(self._fd)
|
tim@3
|
1099 except:
|
tim@3
|
1100 # for the very rare case this might happen
|
tim@3
|
1101 raise OSCClientError("Timed out waiting for file descriptor")
|
tim@3
|
1102
|
tim@3
|
1103 try:
|
tim@3
|
1104 self.socket.connect(address)
|
tim@3
|
1105 self.socket.sendall(msg.getBinary())
|
tim@3
|
1106
|
tim@3
|
1107 if self.client_address:
|
tim@3
|
1108 self.socket.connect(self.client_address)
|
tim@3
|
1109
|
tim@3
|
1110 except socket.error, e:
|
tim@3
|
1111 if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host'
|
tim@3
|
1112 raise e
|
tim@3
|
1113 #elif e[0]==61:
|
tim@3
|
1114 # pass # ADDED BY TIM: 61 = 'Connection refused.'
|
tim@3
|
1115 else:
|
tim@3
|
1116 raise OSCClientError("while sending to %s: %s" % (str(address), str(e)))
|
tim@3
|
1117
|
tim@3
|
1118 def send(self, msg, timeout=None):
|
tim@3
|
1119 """Send the given OSCMessage.
|
tim@3
|
1120 The Client must be already connected.
|
tim@3
|
1121 - msg: OSCMessage (or OSCBundle) to be sent
|
tim@3
|
1122 - timeout: A timeout value for attempting to send. If timeout == None,
|
tim@3
|
1123 this call blocks until socket is available for writing.
|
tim@3
|
1124 Raises OSCClientError when timing out while waiting for the socket,
|
tim@3
|
1125 or when the Client isn't connected to a remote server.
|
tim@3
|
1126 """
|
tim@3
|
1127 if not isinstance(msg, OSCMessage):
|
tim@3
|
1128 raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object")
|
tim@3
|
1129
|
tim@3
|
1130 ret = select.select([],[self._fd], [], timeout)
|
tim@3
|
1131 try:
|
tim@3
|
1132 ret[1].index(self._fd)
|
tim@3
|
1133 except:
|
tim@3
|
1134 # for the very rare case this might happen
|
tim@3
|
1135 raise OSCClientError("Timed out waiting for file descriptor")
|
tim@3
|
1136
|
tim@3
|
1137 try:
|
tim@3
|
1138 self.socket.sendall(msg.getBinary())
|
tim@3
|
1139 except socket.error, e:
|
tim@3
|
1140 if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host'
|
tim@3
|
1141 raise e
|
tim@3
|
1142 else:
|
tim@3
|
1143 raise OSCClientError("while sending: %s" % str(e))
|
tim@3
|
1144
|
tim@3
|
1145 ######
|
tim@3
|
1146 #
|
tim@3
|
1147 # FilterString Utility functions
|
tim@3
|
1148 #
|
tim@3
|
1149 ######
|
tim@3
|
1150
|
tim@3
|
1151 def parseFilterStr(args):
|
tim@3
|
1152 """Convert Message-Filter settings in '+<addr> -<addr> ...' format to a dict of the form
|
tim@3
|
1153 { '<addr>':True, '<addr>':False, ... }
|
tim@3
|
1154 Returns a list: ['<prefix>', filters]
|
tim@3
|
1155 """
|
tim@3
|
1156 out = {}
|
tim@3
|
1157
|
tim@3
|
1158 if type(args) in types.StringTypes:
|
tim@3
|
1159 args = [args]
|
tim@3
|
1160
|
tim@3
|
1161 prefix = None
|
tim@3
|
1162 for arg in args:
|
tim@3
|
1163 head = None
|
tim@3
|
1164 for plus in arg.split('+'):
|
tim@3
|
1165 minus = plus.split('-')
|
tim@3
|
1166 plusfs = minus.pop(0).strip()
|
tim@3
|
1167 if len(plusfs):
|
tim@3
|
1168 plusfs = '/' + plusfs.strip('/')
|
tim@3
|
1169
|
tim@3
|
1170 if (head == None) and (plusfs != "/*"):
|
tim@3
|
1171 head = plusfs
|
tim@3
|
1172 elif len(plusfs):
|
tim@3
|
1173 if plusfs == '/*':
|
tim@3
|
1174 out = { '/*':True } # reset all previous filters
|
tim@3
|
1175 else:
|
tim@3
|
1176 out[plusfs] = True
|
tim@3
|
1177
|
tim@3
|
1178 for minusfs in minus:
|
tim@3
|
1179 minusfs = minusfs.strip()
|
tim@3
|
1180 if len(minusfs):
|
tim@3
|
1181 minusfs = '/' + minusfs.strip('/')
|
tim@3
|
1182 if minusfs == '/*':
|
tim@3
|
1183 out = { '/*':False } # reset all previous filters
|
tim@3
|
1184 else:
|
tim@3
|
1185 out[minusfs] = False
|
tim@3
|
1186
|
tim@3
|
1187 if prefix == None:
|
tim@3
|
1188 prefix = head
|
tim@3
|
1189
|
tim@3
|
1190 return [prefix, out]
|
tim@3
|
1191
|
tim@3
|
1192 def getFilterStr(filters):
|
tim@3
|
1193 """Return the given 'filters' dict as a list of
|
tim@3
|
1194 '+<addr>' | '-<addr>' filter-strings
|
tim@3
|
1195 """
|
tim@3
|
1196 if not len(filters):
|
tim@3
|
1197 return []
|
tim@3
|
1198
|
tim@3
|
1199 if '/*' in filters.keys():
|
tim@3
|
1200 if filters['/*']:
|
tim@3
|
1201 out = ["+/*"]
|
tim@3
|
1202 else:
|
tim@3
|
1203 out = ["-/*"]
|
tim@3
|
1204 else:
|
tim@3
|
1205 if False in filters.values():
|
tim@3
|
1206 out = ["+/*"]
|
tim@3
|
1207 else:
|
tim@3
|
1208 out = ["-/*"]
|
tim@3
|
1209
|
tim@3
|
1210 for (addr, bool) in filters.items():
|
tim@3
|
1211 if addr == '/*':
|
tim@3
|
1212 continue
|
tim@3
|
1213
|
tim@3
|
1214 if bool:
|
tim@3
|
1215 out.append("+%s" % addr)
|
tim@3
|
1216 else:
|
tim@3
|
1217 out.append("-%s" % addr)
|
tim@3
|
1218
|
tim@3
|
1219 return out
|
tim@3
|
1220
|
tim@3
|
1221 # A translation-table for mapping OSC-address expressions to Python 're' expressions
|
tim@3
|
1222 OSCtrans = string.maketrans("{,}?","(|).")
|
tim@3
|
1223
|
tim@3
|
1224 def getRegEx(pattern):
|
tim@3
|
1225 """Compiles and returns a 'regular expression' object for the given address-pattern.
|
tim@3
|
1226 """
|
tim@3
|
1227 # Translate OSC-address syntax to python 're' syntax
|
tim@3
|
1228 pattern = pattern.replace(".", r"\.") # first, escape all '.'s in the pattern.
|
tim@3
|
1229 pattern = pattern.replace("(", r"\(") # escape all '('s.
|
tim@3
|
1230 pattern = pattern.replace(")", r"\)") # escape all ')'s.
|
tim@3
|
1231 pattern = pattern.replace("*", r".*") # replace a '*' by '.*' (match 0 or more characters)
|
tim@3
|
1232 pattern = pattern.translate(OSCtrans) # change '?' to '.' and '{,}' to '(|)'
|
tim@3
|
1233
|
tim@3
|
1234 return re.compile(pattern)
|
tim@3
|
1235
|
tim@3
|
1236 ######
|
tim@3
|
1237 #
|
tim@3
|
1238 # OSCMultiClient class
|
tim@3
|
1239 #
|
tim@3
|
1240 ######
|
tim@3
|
1241
|
tim@3
|
1242 class OSCMultiClient(OSCClient):
|
tim@3
|
1243 """'Multiple-Unicast' OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket
|
tim@3
|
1244 This client keeps a dict of 'OSCTargets'. and sends each OSCMessage to each OSCTarget
|
tim@3
|
1245 The OSCTargets are simply (host, port) tuples, and may be associated with an OSC-address prefix.
|
tim@3
|
1246 the OSCTarget's prefix gets prepended to each OSCMessage sent to that target.
|
tim@3
|
1247 """
|
tim@3
|
1248 def __init__(self, server=None):
|
tim@3
|
1249 """Construct a "Multi" OSC Client.
|
tim@3
|
1250 - server: Local OSCServer-instance this client will use the socket of for transmissions.
|
tim@3
|
1251 If none is supplied, a socket will be created.
|
tim@3
|
1252 """
|
tim@3
|
1253 super(OSCMultiClient, self).__init__(server)
|
tim@3
|
1254
|
tim@3
|
1255 self.targets = {}
|
tim@3
|
1256
|
tim@3
|
1257 def _searchHostAddr(self, host):
|
tim@3
|
1258 """Search the subscribed OSCTargets for (the first occurence of) given host.
|
tim@3
|
1259 Returns a (host, port) tuple
|
tim@3
|
1260 """
|
tim@3
|
1261 try:
|
tim@3
|
1262 host = socket.gethostbyname(host)
|
tim@3
|
1263 except socket.error:
|
tim@3
|
1264 pass
|
tim@3
|
1265
|
tim@3
|
1266 for addr in self.targets.keys():
|
tim@3
|
1267 if host == addr[0]:
|
tim@3
|
1268 return addr
|
tim@3
|
1269
|
tim@3
|
1270 raise NotSubscribedError((host, None))
|
tim@3
|
1271
|
tim@3
|
1272 def _updateFilters(self, dst, src):
|
tim@3
|
1273 """Update a 'filters' dict with values form another 'filters' dict:
|
tim@3
|
1274 - src[a] == True and dst[a] == False: del dst[a]
|
tim@3
|
1275 - src[a] == False and dst[a] == True: del dst[a]
|
tim@3
|
1276 - a not in dst: dst[a] == src[a]
|
tim@3
|
1277 """
|
tim@3
|
1278 if '/*' in src.keys(): # reset filters
|
tim@3
|
1279 dst.clear() # 'match everything' == no filters
|
tim@3
|
1280 if not src.pop('/*'):
|
tim@3
|
1281 dst['/*'] = False # 'match nothing'
|
tim@3
|
1282
|
tim@3
|
1283 for (addr, bool) in src.items():
|
tim@3
|
1284 if (addr in dst.keys()) and (dst[addr] != bool):
|
tim@3
|
1285 del dst[addr]
|
tim@3
|
1286 else:
|
tim@3
|
1287 dst[addr] = bool
|
tim@3
|
1288
|
tim@3
|
1289 def _setTarget(self, address, prefix=None, filters=None):
|
tim@3
|
1290 """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget.
|
tim@3
|
1291 - address ((host, port) tuple): IP-address & UDP-port
|
tim@3
|
1292 - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage
|
tim@3
|
1293 sent to this OSCTarget (optional)
|
tim@3
|
1294 """
|
tim@3
|
1295 if address not in self.targets.keys():
|
tim@3
|
1296 self.targets[address] = ["",{}]
|
tim@3
|
1297
|
tim@3
|
1298 if prefix != None:
|
tim@3
|
1299 if len(prefix):
|
tim@3
|
1300 # make sure prefix starts with ONE '/', and does not end with '/'
|
tim@3
|
1301 prefix = '/' + prefix.strip('/')
|
tim@3
|
1302
|
tim@3
|
1303 self.targets[address][0] = prefix
|
tim@3
|
1304
|
tim@3
|
1305 if filters != None:
|
tim@3
|
1306 if type(filters) in types.StringTypes:
|
tim@3
|
1307 (_, filters) = parseFilterStr(filters)
|
tim@3
|
1308 elif type(filters) != types.DictType:
|
tim@3
|
1309 raise TypeError("'filters' argument must be a dict with {addr:bool} entries")
|
tim@3
|
1310
|
tim@3
|
1311 self._updateFilters(self.targets[address][1], filters)
|
tim@3
|
1312
|
tim@3
|
1313 def setOSCTarget(self, address, prefix=None, filters=None):
|
tim@3
|
1314 """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget.
|
tim@3
|
1315 the 'address' argument can be a ((host, port) tuple) : The target server address & UDP-port
|
tim@3
|
1316 or a 'host' (string) : The host will be looked-up
|
tim@3
|
1317 - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage
|
tim@3
|
1318 sent to this OSCTarget (optional)
|
tim@3
|
1319 """
|
tim@3
|
1320 if type(address) in types.StringTypes:
|
tim@3
|
1321 address = self._searchHostAddr(address)
|
tim@3
|
1322
|
tim@3
|
1323 elif (type(address) == types.TupleType):
|
tim@3
|
1324 (host, port) = address[:2]
|
tim@3
|
1325 try:
|
tim@3
|
1326 host = socket.gethostbyname(host)
|
tim@3
|
1327 except:
|
tim@3
|
1328 pass
|
tim@3
|
1329
|
tim@3
|
1330 address = (host, port)
|
tim@3
|
1331 else:
|
tim@3
|
1332 raise TypeError("'address' argument must be a (host, port) tuple or a 'host' string")
|
tim@3
|
1333
|
tim@3
|
1334 self._setTarget(address, prefix, filters)
|
tim@3
|
1335
|
tim@3
|
1336 def setOSCTargetFromStr(self, url):
|
tim@3
|
1337 """Adds or modifies a subscribed OSCTarget from the given string, which should be in the
|
tim@3
|
1338 '<host>:<port>[/<prefix>] [+/<filter>]|[-/<filter>] ...' format.
|
tim@3
|
1339 """
|
tim@3
|
1340 (addr, tail) = parseUrlStr(url)
|
tim@3
|
1341 (prefix, filters) = parseFilterStr(tail)
|
tim@3
|
1342 self._setTarget(addr, prefix, filters)
|
tim@3
|
1343
|
tim@3
|
1344 def _delTarget(self, address, prefix=None):
|
tim@3
|
1345 """Delete the specified OSCTarget from the Client's dict.
|
tim@3
|
1346 the 'address' argument must be a (host, port) tuple.
|
tim@3
|
1347 If the 'prefix' argument is given, the Target is only deleted if the address and prefix match.
|
tim@3
|
1348 """
|
tim@3
|
1349 try:
|
tim@3
|
1350 if prefix == None:
|
tim@3
|
1351 del self.targets[address]
|
tim@3
|
1352 elif prefix == self.targets[address][0]:
|
tim@3
|
1353 del self.targets[address]
|
tim@3
|
1354 except KeyError:
|
tim@3
|
1355 raise NotSubscribedError(address, prefix)
|
tim@3
|
1356
|
tim@3
|
1357 def delOSCTarget(self, address, prefix=None):
|
tim@3
|
1358 """Delete the specified OSCTarget from the Client's dict.
|
tim@3
|
1359 the 'address' argument can be a ((host, port) tuple), or a hostname.
|
tim@3
|
1360 If the 'prefix' argument is given, the Target is only deleted if the address and prefix match.
|
tim@3
|
1361 """
|
tim@3
|
1362 if type(address) in types.StringTypes:
|
tim@3
|
1363 address = self._searchHostAddr(address)
|
tim@3
|
1364
|
tim@3
|
1365 if type(address) == types.TupleType:
|
tim@3
|
1366 (host, port) = address[:2]
|
tim@3
|
1367 try:
|
tim@3
|
1368 host = socket.gethostbyname(host)
|
tim@3
|
1369 except socket.error:
|
tim@3
|
1370 pass
|
tim@3
|
1371 address = (host, port)
|
tim@3
|
1372
|
tim@3
|
1373 self._delTarget(address, prefix)
|
tim@3
|
1374
|
tim@3
|
1375 def hasOSCTarget(self, address, prefix=None):
|
tim@3
|
1376 """Return True if the given OSCTarget exists in the Client's dict.
|
tim@3
|
1377 the 'address' argument can be a ((host, port) tuple), or a hostname.
|
tim@3
|
1378 If the 'prefix' argument is given, the return-value is only True if the address and prefix match.
|
tim@3
|
1379 """
|
tim@3
|
1380 if type(address) in types.StringTypes:
|
tim@3
|
1381 address = self._searchHostAddr(address)
|
tim@3
|
1382
|
tim@3
|
1383 if type(address) == types.TupleType:
|
tim@3
|
1384 (host, port) = address[:2]
|
tim@3
|
1385 try:
|
tim@3
|
1386 host = socket.gethostbyname(host)
|
tim@3
|
1387 except socket.error:
|
tim@3
|
1388 pass
|
tim@3
|
1389 address = (host, port)
|
tim@3
|
1390
|
tim@3
|
1391 if address in self.targets.keys():
|
tim@3
|
1392 if prefix == None:
|
tim@3
|
1393 return True
|
tim@3
|
1394 elif prefix == self.targets[address][0]:
|
tim@3
|
1395 return True
|
tim@3
|
1396
|
tim@3
|
1397 return False
|
tim@3
|
1398
|
tim@3
|
1399 def getOSCTargets(self):
|
tim@3
|
1400 """Returns the dict of OSCTargets: {addr:[prefix, filters], ...}
|
tim@3
|
1401 """
|
tim@3
|
1402 out = {}
|
tim@3
|
1403 for ((host, port), pf) in self.targets.items():
|
tim@3
|
1404 try:
|
tim@3
|
1405 (host, _, _) = socket.gethostbyaddr(host)
|
tim@3
|
1406 except socket.error:
|
tim@3
|
1407 pass
|
tim@3
|
1408
|
tim@3
|
1409 out[(host, port)] = pf
|
tim@3
|
1410
|
tim@3
|
1411 return out
|
tim@3
|
1412
|
tim@3
|
1413 def getOSCTarget(self, address):
|
tim@3
|
1414 """Returns the OSCTarget matching the given address as a ((host, port), [prefix, filters]) tuple.
|
tim@3
|
1415 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned
|
tim@3
|
1416 Returns (None, ['',{}]) if address not found.
|
tim@3
|
1417 """
|
tim@3
|
1418 if type(address) in types.StringTypes:
|
tim@3
|
1419 address = self._searchHostAddr(address)
|
tim@3
|
1420
|
tim@3
|
1421 if (type(address) == types.TupleType):
|
tim@3
|
1422 (host, port) = address[:2]
|
tim@3
|
1423 try:
|
tim@3
|
1424 host = socket.gethostbyname(host)
|
tim@3
|
1425 except socket.error:
|
tim@3
|
1426 pass
|
tim@3
|
1427 address = (host, port)
|
tim@3
|
1428
|
tim@3
|
1429 if (address in self.targets.keys()):
|
tim@3
|
1430 try:
|
tim@3
|
1431 (host, _, _) = socket.gethostbyaddr(host)
|
tim@3
|
1432 except socket.error:
|
tim@3
|
1433 pass
|
tim@3
|
1434
|
tim@3
|
1435 return ((host, port), self.targets[address])
|
tim@3
|
1436
|
tim@3
|
1437 return (None, ['',{}])
|
tim@3
|
1438
|
tim@3
|
1439 def clearOSCTargets(self):
|
tim@3
|
1440 """Erases all OSCTargets from the Client's dict
|
tim@3
|
1441 """
|
tim@3
|
1442 self.targets = {}
|
tim@3
|
1443
|
tim@3
|
1444 def updateOSCTargets(self, dict):
|
tim@3
|
1445 """Update the Client's OSCTargets dict with the contents of 'dict'
|
tim@3
|
1446 The given dict's items MUST be of the form
|
tim@3
|
1447 { (host, port):[prefix, filters], ... }
|
tim@3
|
1448 """
|
tim@3
|
1449 for ((host, port), (prefix, filters)) in dict.items():
|
tim@3
|
1450 val = [prefix, {}]
|
tim@3
|
1451 self._updateFilters(val[1], filters)
|
tim@3
|
1452
|
tim@3
|
1453 try:
|
tim@3
|
1454 host = socket.gethostbyname(host)
|
tim@3
|
1455 except socket.error:
|
tim@3
|
1456 pass
|
tim@3
|
1457
|
tim@3
|
1458 self.targets[(host, port)] = val
|
tim@3
|
1459
|
tim@3
|
1460 def getOSCTargetStr(self, address):
|
tim@3
|
1461 """Returns the OSCTarget matching the given address as a ('osc://<host>:<port>[<prefix>]', ['<filter-string>', ...])' tuple.
|
tim@3
|
1462 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned
|
tim@3
|
1463 Returns (None, []) if address not found.
|
tim@3
|
1464 """
|
tim@3
|
1465 (addr, (prefix, filters)) = self.getOSCTarget(address)
|
tim@3
|
1466 if addr == None:
|
tim@3
|
1467 return (None, [])
|
tim@3
|
1468
|
tim@3
|
1469 return ("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters))
|
tim@3
|
1470
|
tim@3
|
1471 def getOSCTargetStrings(self):
|
tim@3
|
1472 """Returns a list of all OSCTargets as ('osc://<host>:<port>[<prefix>]', ['<filter-string>', ...])' tuples.
|
tim@3
|
1473 """
|
tim@3
|
1474 out = []
|
tim@3
|
1475 for (addr, (prefix, filters)) in self.targets.items():
|
tim@3
|
1476 out.append(("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters)))
|
tim@3
|
1477
|
tim@3
|
1478 return out
|
tim@3
|
1479
|
tim@3
|
1480 def connect(self, address):
|
tim@3
|
1481 """The OSCMultiClient isn't allowed to connect to any specific
|
tim@3
|
1482 address.
|
tim@3
|
1483 """
|
tim@3
|
1484 return NotImplemented
|
tim@3
|
1485
|
tim@3
|
1486 def sendto(self, msg, address, timeout=None):
|
tim@3
|
1487 """Send the given OSCMessage.
|
tim@3
|
1488 The specified address is ignored. Instead this method calls send() to
|
tim@3
|
1489 send the message to all subscribed clients.
|
tim@3
|
1490 - msg: OSCMessage (or OSCBundle) to be sent
|
tim@3
|
1491 - address: (host, port) tuple specifing remote server to send the message to
|
tim@3
|
1492 - timeout: A timeout value for attempting to send. If timeout == None,
|
tim@3
|
1493 this call blocks until socket is available for writing.
|
tim@3
|
1494 Raises OSCClientError when timing out while waiting for the socket.
|
tim@3
|
1495 """
|
tim@3
|
1496 self.send(msg, timeout)
|
tim@3
|
1497
|
tim@3
|
1498 def _filterMessage(self, filters, msg):
|
tim@3
|
1499 """Checks the given OSCMessge against the given filters.
|
tim@3
|
1500 'filters' is a dict containing OSC-address:bool pairs.
|
tim@3
|
1501 If 'msg' is an OSCBundle, recursively filters its constituents.
|
tim@3
|
1502 Returns None if the message is to be filtered, else returns the message.
|
tim@3
|
1503 or
|
tim@3
|
1504 Returns a copy of the OSCBundle with the filtered messages removed.
|
tim@3
|
1505 """
|
tim@3
|
1506 if isinstance(msg, OSCBundle):
|
tim@3
|
1507 out = msg.copy()
|
tim@3
|
1508 msgs = out.values()
|
tim@3
|
1509 out.clearData()
|
tim@3
|
1510 for m in msgs:
|
tim@3
|
1511 m = self._filterMessage(filters, m)
|
tim@3
|
1512 if m: # this catches 'None' and empty bundles.
|
tim@3
|
1513 out.append(m)
|
tim@3
|
1514
|
tim@3
|
1515 elif isinstance(msg, OSCMessage):
|
tim@3
|
1516 if '/*' in filters.keys():
|
tim@3
|
1517 if filters['/*']:
|
tim@3
|
1518 out = msg
|
tim@3
|
1519 else:
|
tim@3
|
1520 out = None
|
tim@3
|
1521
|
tim@3
|
1522 elif False in filters.values():
|
tim@3
|
1523 out = msg
|
tim@3
|
1524 else:
|
tim@3
|
1525 out = None
|
tim@3
|
1526
|
tim@3
|
1527 else:
|
tim@3
|
1528 raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object")
|
tim@3
|
1529
|
tim@3
|
1530 expr = getRegEx(msg.address)
|
tim@3
|
1531
|
tim@3
|
1532 for addr in filters.keys():
|
tim@3
|
1533 if addr == '/*':
|
tim@3
|
1534 continue
|
tim@3
|
1535
|
tim@3
|
1536 match = expr.match(addr)
|
tim@3
|
1537 if match and (match.end() == len(addr)):
|
tim@3
|
1538 if filters[addr]:
|
tim@3
|
1539 out = msg
|
tim@3
|
1540 else:
|
tim@3
|
1541 out = None
|
tim@3
|
1542 break
|
tim@3
|
1543
|
tim@3
|
1544 return out
|
tim@3
|
1545
|
tim@3
|
1546 def _prefixAddress(self, prefix, msg):
|
tim@3
|
1547 """Makes a copy of the given OSCMessage, then prepends the given prefix to
|
tim@3
|
1548 The message's OSC-address.
|
tim@3
|
1549 If 'msg' is an OSCBundle, recursively prepends the prefix to its constituents.
|
tim@3
|
1550 """
|
tim@3
|
1551 out = msg.copy()
|
tim@3
|
1552
|
tim@3
|
1553 if isinstance(msg, OSCBundle):
|
tim@3
|
1554 msgs = out.values()
|
tim@3
|
1555 out.clearData()
|
tim@3
|
1556 for m in msgs:
|
tim@3
|
1557 out.append(self._prefixAddress(prefix, m))
|
tim@3
|
1558
|
tim@3
|
1559 elif isinstance(msg, OSCMessage):
|
tim@3
|
1560 out.setAddress(prefix + out.address)
|
tim@3
|
1561
|
tim@3
|
1562 else:
|
tim@3
|
1563 raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object")
|
tim@3
|
1564
|
tim@3
|
1565 return out
|
tim@3
|
1566
|
tim@3
|
1567 def send(self, msg, timeout=None):
|
tim@3
|
1568 """Send the given OSCMessage to all subscribed OSCTargets
|
tim@3
|
1569 - msg: OSCMessage (or OSCBundle) to be sent
|
tim@3
|
1570 - timeout: A timeout value for attempting to send. If timeout == None,
|
tim@3
|
1571 this call blocks until socket is available for writing.
|
tim@3
|
1572 Raises OSCClientError when timing out while waiting for the socket.
|
tim@3
|
1573 """
|
tim@3
|
1574 for (address, (prefix, filters)) in self.targets.items():
|
tim@3
|
1575 if len(filters):
|
tim@3
|
1576 out = self._filterMessage(filters, msg)
|
tim@3
|
1577 if not out: # this catches 'None' and empty bundles.
|
tim@3
|
1578 continue
|
tim@3
|
1579 else:
|
tim@3
|
1580 out = msg
|
tim@3
|
1581
|
tim@3
|
1582 if len(prefix):
|
tim@3
|
1583 out = self._prefixAddress(prefix, msg)
|
tim@3
|
1584
|
tim@3
|
1585 binary = out.getBinary()
|
tim@3
|
1586
|
tim@3
|
1587 ret = select.select([],[self._fd], [], timeout)
|
tim@3
|
1588 try:
|
tim@3
|
1589 ret[1].index(self._fd)
|
tim@3
|
1590 except:
|
tim@3
|
1591 # for the very rare case this might happen
|
tim@3
|
1592 raise OSCClientError("Timed out waiting for file descriptor")
|
tim@3
|
1593
|
tim@3
|
1594 try:
|
tim@3
|
1595 while len(binary):
|
tim@3
|
1596 sent = self.socket.sendto(binary, address)
|
tim@3
|
1597 binary = binary[sent:]
|
tim@3
|
1598
|
tim@3
|
1599 except socket.error, e:
|
tim@3
|
1600 if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host'
|
tim@3
|
1601 raise e
|
tim@3
|
1602 else:
|
tim@3
|
1603 raise OSCClientError("while sending to %s: %s" % (str(address), str(e)))
|
tim@3
|
1604
|
tim@3
|
1605 ######
|
tim@3
|
1606 #
|
tim@3
|
1607 # OSCRequestHandler classes
|
tim@3
|
1608 #
|
tim@3
|
1609 ######
|
tim@3
|
1610
|
tim@3
|
1611 class OSCRequestHandler(DatagramRequestHandler):
|
tim@3
|
1612 """RequestHandler class for the OSCServer
|
tim@3
|
1613 """
|
tim@3
|
1614 def dispatchMessage(self, pattern, tags, data):
|
tim@3
|
1615 """Attmept to match the given OSC-address pattern, which may contain '*',
|
tim@3
|
1616 against all callbacks registered with the OSCServer.
|
tim@3
|
1617 Calls the matching callback and returns whatever it returns.
|
tim@3
|
1618 If no match is found, and a 'default' callback is registered, it calls that one,
|
tim@3
|
1619 or raises NoCallbackError if a 'default' callback is not registered.
|
tim@3
|
1620
|
tim@3
|
1621 - pattern (string): The OSC-address of the receied message
|
tim@3
|
1622 - tags (string): The OSC-typetags of the receied message's arguments, without ','
|
tim@3
|
1623 - data (list): The message arguments
|
tim@3
|
1624 """
|
tim@3
|
1625 if len(tags) != len(data):
|
tim@3
|
1626 raise OSCServerError("Malformed OSC-message; got %d typetags [%s] vs. %d values" % (len(tags), tags, len(data)))
|
tim@3
|
1627
|
tim@3
|
1628 expr = getRegEx(pattern)
|
tim@3
|
1629
|
tim@3
|
1630 replies = []
|
tim@3
|
1631 matched = 0
|
tim@3
|
1632 for addr in self.server.callbacks.keys():
|
tim@3
|
1633 match = expr.match(addr)
|
tim@3
|
1634 if match and (match.end() == len(addr)):
|
tim@3
|
1635 reply = self.server.callbacks[addr](pattern, tags, data, self.client_address)
|
tim@3
|
1636 matched += 1
|
tim@3
|
1637 if isinstance(reply, OSCMessage):
|
tim@3
|
1638 replies.append(reply)
|
tim@3
|
1639 elif reply != None:
|
tim@3
|
1640 raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks[addr], type(reply)))
|
tim@3
|
1641
|
tim@3
|
1642 if matched == 0:
|
tim@3
|
1643 if 'default' in self.server.callbacks:
|
tim@3
|
1644 reply = self.server.callbacks['default'](pattern, tags, data, self.client_address)
|
tim@3
|
1645 if isinstance(reply, OSCMessage):
|
tim@3
|
1646 replies.append(reply)
|
tim@3
|
1647 elif reply != None:
|
tim@3
|
1648 raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks['default'], type(reply)))
|
tim@3
|
1649 else:
|
tim@3
|
1650 raise NoCallbackError(pattern)
|
tim@3
|
1651
|
tim@3
|
1652 return replies
|
tim@3
|
1653
|
tim@3
|
1654 def setup(self):
|
tim@3
|
1655 """Prepare RequestHandler.
|
tim@3
|
1656 Unpacks request as (packet, source socket address)
|
tim@3
|
1657 Creates an empty list for replies.
|
tim@3
|
1658 """
|
tim@3
|
1659 (self.packet, self.socket) = self.request
|
tim@3
|
1660 self.replies = []
|
tim@3
|
1661
|
tim@3
|
1662 def _unbundle(self, decoded):
|
tim@3
|
1663 """Recursive bundle-unpacking function"""
|
tim@3
|
1664 if decoded[0] != "#bundle":
|
tim@3
|
1665 self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:])
|
tim@3
|
1666 return
|
tim@3
|
1667
|
tim@3
|
1668 now = time.time()
|
tim@3
|
1669 timetag = decoded[1]
|
tim@3
|
1670 if (timetag > 0.) and (timetag > now):
|
tim@3
|
1671 time.sleep(timetag - now)
|
tim@3
|
1672
|
tim@3
|
1673 for msg in decoded[2:]:
|
tim@3
|
1674 self._unbundle(msg)
|
tim@3
|
1675
|
tim@3
|
1676 def handle(self):
|
tim@3
|
1677 """Handle incoming OSCMessage
|
tim@3
|
1678 """
|
tim@3
|
1679 decoded = decodeOSC(self.packet)
|
tim@3
|
1680 if not len(decoded):
|
tim@3
|
1681 return
|
tim@3
|
1682
|
tim@3
|
1683 self._unbundle(decoded)
|
tim@3
|
1684
|
tim@3
|
1685 def finish(self):
|
tim@3
|
1686 """Finish handling OSCMessage.
|
tim@3
|
1687 Send any reply returned by the callback(s) back to the originating client
|
tim@3
|
1688 as an OSCMessage or OSCBundle
|
tim@3
|
1689 """
|
tim@3
|
1690 if self.server.return_port:
|
tim@3
|
1691 self.client_address = (self.client_address[0], self.server.return_port)
|
tim@3
|
1692
|
tim@3
|
1693 if len(self.replies) > 1:
|
tim@3
|
1694 msg = OSCBundle()
|
tim@3
|
1695 for reply in self.replies:
|
tim@3
|
1696 msg.append(reply)
|
tim@3
|
1697 elif len(self.replies) == 1:
|
tim@3
|
1698 msg = self.replies[0]
|
tim@3
|
1699 else:
|
tim@3
|
1700 return
|
tim@3
|
1701
|
tim@3
|
1702 self.server.client.sendto(msg, self.client_address)
|
tim@3
|
1703
|
tim@3
|
1704 class ThreadingOSCRequestHandler(OSCRequestHandler):
|
tim@3
|
1705 """Multi-threaded OSCRequestHandler;
|
tim@3
|
1706 Starts a new RequestHandler thread for each unbundled OSCMessage
|
tim@3
|
1707 """
|
tim@3
|
1708 def _unbundle(self, decoded):
|
tim@3
|
1709 """Recursive bundle-unpacking function
|
tim@3
|
1710 This version starts a new thread for each sub-Bundle found in the Bundle,
|
tim@3
|
1711 then waits for all its children to finish.
|
tim@3
|
1712 """
|
tim@3
|
1713 if decoded[0] != "#bundle":
|
tim@3
|
1714 self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:])
|
tim@3
|
1715 return
|
tim@3
|
1716
|
tim@3
|
1717 now = time.time()
|
tim@3
|
1718 timetag = decoded[1]
|
tim@3
|
1719 if (timetag > 0.) and (timetag > now):
|
tim@3
|
1720 time.sleep(timetag - now)
|
tim@3
|
1721 now = time.time()
|
tim@3
|
1722
|
tim@3
|
1723 children = []
|
tim@3
|
1724
|
tim@3
|
1725 for msg in decoded[2:]:
|
tim@3
|
1726 t = threading.Thread(target = self._unbundle, args = (msg,))
|
tim@3
|
1727 t.start()
|
tim@3
|
1728 children.append(t)
|
tim@3
|
1729
|
tim@3
|
1730 # wait for all children to terminate
|
tim@3
|
1731 for t in children:
|
tim@3
|
1732 t.join()
|
tim@3
|
1733
|
tim@3
|
1734 ######
|
tim@3
|
1735 #
|
tim@3
|
1736 # OSCServer classes
|
tim@3
|
1737 #
|
tim@3
|
1738 ######
|
tim@3
|
1739
|
tim@3
|
1740 class OSCServer(UDPServer):
|
tim@3
|
1741 """A Synchronous OSCServer
|
tim@3
|
1742 Serves one request at-a-time, until the OSCServer is closed.
|
tim@3
|
1743 The OSC address-pattern is matched against a set of OSC-adresses
|
tim@3
|
1744 that have been registered to the server with a callback-function.
|
tim@3
|
1745 If the adress-pattern of the message machtes the registered address of a callback,
|
tim@3
|
1746 that function is called.
|
tim@3
|
1747 """
|
tim@3
|
1748
|
tim@3
|
1749 # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer
|
tim@3
|
1750 RequestHandlerClass = OSCRequestHandler
|
tim@3
|
1751
|
tim@3
|
1752 # define a socket timeout, so the serve_forever loop can actually exit.
|
tim@3
|
1753 socket_timeout = 1
|
tim@3
|
1754
|
tim@3
|
1755 # DEBUG: print error-tracebacks (to stderr)?
|
tim@3
|
1756 print_tracebacks = False
|
tim@3
|
1757
|
tim@3
|
1758 def __init__(self, server_address, client=None, return_port=0):
|
tim@3
|
1759 """Instantiate an OSCServer.
|
tim@3
|
1760 - server_address ((host, port) tuple): the local host & UDP-port
|
tim@3
|
1761 the server listens on
|
tim@3
|
1762 - client (OSCClient instance): The OSCClient used to send replies from this server.
|
tim@3
|
1763 If none is supplied (default) an OSCClient will be created.
|
tim@3
|
1764 - return_port (int): if supplied, sets the default UDP destination-port
|
tim@3
|
1765 for replies coming from this server.
|
tim@3
|
1766 """
|
tim@3
|
1767 UDPServer.__init__(self, server_address, self.RequestHandlerClass)
|
tim@3
|
1768
|
tim@3
|
1769 self.callbacks = {}
|
tim@3
|
1770 self.setReturnPort(return_port)
|
tim@3
|
1771 self.error_prefix = ""
|
tim@3
|
1772 self.info_prefix = "/info"
|
tim@3
|
1773
|
tim@3
|
1774 self.socket.settimeout(self.socket_timeout)
|
tim@3
|
1775
|
tim@3
|
1776 self.running = False
|
tim@3
|
1777 self.client = None
|
tim@3
|
1778
|
tim@3
|
1779 if client == None:
|
tim@3
|
1780 self.client = OSCClient(server=self)
|
tim@3
|
1781 else:
|
tim@3
|
1782 self.setClient(client)
|
tim@3
|
1783
|
tim@3
|
1784 def setClient(self, client):
|
tim@3
|
1785 """Associate this Server with a new local Client instance, closing the Client this Server is currently using.
|
tim@3
|
1786 """
|
tim@3
|
1787 if not isinstance(client, OSCClient):
|
tim@3
|
1788 raise ValueError("'client' argument is not a valid OSCClient object")
|
tim@3
|
1789
|
tim@3
|
1790 if client.server != None:
|
tim@3
|
1791 raise OSCServerError("Provided OSCClient already has an OSCServer-instance: %s" % str(client.server))
|
tim@3
|
1792
|
tim@3
|
1793 # Server socket is already listening at this point, so we can't use the client's socket.
|
tim@3
|
1794 # we'll have to force our socket on the client...
|
tim@3
|
1795 client_address = client.address() # client may be already connected
|
tim@3
|
1796 client.close() # shut-down that socket
|
tim@3
|
1797
|
tim@3
|
1798 # force our socket upon the client
|
tim@3
|
1799 client.socket = self.socket.dup()
|
tim@3
|
1800 client.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, client.sndbuf_size)
|
tim@3
|
1801 client._fd = client.socket.fileno()
|
tim@3
|
1802 client.server = self
|
tim@3
|
1803
|
tim@3
|
1804 if client_address:
|
tim@3
|
1805 client.connect(client_address)
|
tim@3
|
1806 if not self.return_port:
|
tim@3
|
1807 self.return_port = client_address[1]
|
tim@3
|
1808
|
tim@3
|
1809 if self.client != None:
|
tim@3
|
1810 self.client.close()
|
tim@3
|
1811
|
tim@3
|
1812 self.client = client
|
tim@3
|
1813
|
tim@3
|
1814 def serve_forever(self):
|
tim@3
|
1815 """Handle one request at a time until server is closed."""
|
tim@3
|
1816 self.running = True
|
tim@3
|
1817 while self.running:
|
tim@3
|
1818 self.handle_request() # this times-out when no data arrives.
|
tim@3
|
1819
|
tim@3
|
1820 def close(self):
|
tim@3
|
1821 """Stops serving requests, closes server (socket), closes used client
|
tim@3
|
1822 """
|
tim@3
|
1823 self.running = False
|
tim@3
|
1824 self.client.close()
|
tim@3
|
1825 self.server_close()
|
tim@3
|
1826
|
tim@3
|
1827 def __str__(self):
|
tim@3
|
1828 """Returns a string containing this Server's Class-name, software-version and local bound address (if any)
|
tim@3
|
1829 """
|
tim@3
|
1830 out = self.__class__.__name__
|
tim@3
|
1831 out += " v%s.%s-%s" % version
|
tim@3
|
1832 addr = self.address()
|
tim@3
|
1833 if addr:
|
tim@3
|
1834 out += " listening on osc://%s" % getUrlStr(addr)
|
tim@3
|
1835 else:
|
tim@3
|
1836 out += " (unbound)"
|
tim@3
|
1837
|
tim@3
|
1838 return out
|
tim@3
|
1839
|
tim@3
|
1840 def __eq__(self, other):
|
tim@3
|
1841 """Compare function.
|
tim@3
|
1842 """
|
tim@3
|
1843 if not isinstance(other, self.__class__):
|
tim@3
|
1844 return False
|
tim@3
|
1845
|
tim@3
|
1846 return cmp(self.socket._sock, other.socket._sock)
|
tim@3
|
1847
|
tim@3
|
1848 def __ne__(self, other):
|
tim@3
|
1849 """Compare function.
|
tim@3
|
1850 """
|
tim@3
|
1851 return not self.__eq__(other)
|
tim@3
|
1852
|
tim@3
|
1853 def address(self):
|
tim@3
|
1854 """Returns a (host,port) tuple of the local address this server is bound to,
|
tim@3
|
1855 or None if not bound to any address.
|
tim@3
|
1856 """
|
tim@3
|
1857 try:
|
tim@3
|
1858 return self.socket.getsockname()
|
tim@3
|
1859 except socket.error:
|
tim@3
|
1860 return None
|
tim@3
|
1861
|
tim@3
|
1862 def setReturnPort(self, port):
|
tim@3
|
1863 """Set the destination UDP-port for replies returning from this server to the remote client
|
tim@3
|
1864 """
|
tim@3
|
1865 if (port > 1024) and (port < 65536):
|
tim@3
|
1866 self.return_port = port
|
tim@3
|
1867 else:
|
tim@3
|
1868 self.return_port = None
|
tim@3
|
1869
|
tim@3
|
1870
|
tim@3
|
1871 def setSrvInfoPrefix(self, pattern):
|
tim@3
|
1872 """Set the first part of OSC-address (pattern) this server will use to reply to server-info requests.
|
tim@3
|
1873 """
|
tim@3
|
1874 if len(pattern):
|
tim@3
|
1875 pattern = '/' + pattern.strip('/')
|
tim@3
|
1876
|
tim@3
|
1877 self.info_prefix = pattern
|
tim@3
|
1878
|
tim@3
|
1879 def setSrvErrorPrefix(self, pattern=""):
|
tim@3
|
1880 """Set the OSC-address (pattern) this server will use to report errors occuring during
|
tim@3
|
1881 received message handling to the remote client.
|
tim@3
|
1882
|
tim@3
|
1883 If pattern is empty (default), server-errors are not reported back to the client.
|
tim@3
|
1884 """
|
tim@3
|
1885 if len(pattern):
|
tim@3
|
1886 pattern = '/' + pattern.strip('/')
|
tim@3
|
1887
|
tim@3
|
1888 self.error_prefix = pattern
|
tim@3
|
1889
|
tim@3
|
1890 def addMsgHandler(self, address, callback):
|
tim@3
|
1891 """Register a handler for an OSC-address
|
tim@3
|
1892 - 'address' is the OSC address-string.
|
tim@3
|
1893 the address-string should start with '/' and may not contain '*'
|
tim@3
|
1894 - 'callback' is the function called for incoming OSCMessages that match 'address'.
|
tim@3
|
1895 The callback-function will be called with the same arguments as the 'msgPrinter_handler' below
|
tim@3
|
1896 """
|
tim@3
|
1897 for chk in '*?,[]{}# ':
|
tim@3
|
1898 if chk in address:
|
tim@3
|
1899 raise OSCServerError("OSC-address string may not contain any characters in '*?,[]{}# '")
|
tim@3
|
1900
|
tim@3
|
1901 if type(callback) not in (types.FunctionType, types.MethodType):
|
tim@3
|
1902 raise OSCServerError("Message callback '%s' is not callable" % repr(callback))
|
tim@3
|
1903
|
tim@3
|
1904 if address != 'default':
|
tim@3
|
1905 address = '/' + address.strip('/')
|
tim@3
|
1906
|
tim@3
|
1907 self.callbacks[address] = callback
|
tim@3
|
1908
|
tim@3
|
1909 def delMsgHandler(self,address):
|
tim@3
|
1910 """Remove the registered handler for the given OSC-address
|
tim@3
|
1911 """
|
tim@3
|
1912 del self.callbacks[address]
|
tim@3
|
1913
|
tim@3
|
1914 def getOSCAddressSpace(self):
|
tim@3
|
1915 """Returns a list containing all OSC-addresses registerd with this Server.
|
tim@3
|
1916 """
|
tim@3
|
1917 return self.callbacks.keys()
|
tim@3
|
1918
|
tim@3
|
1919 def addDefaultHandlers(self, prefix="", info_prefix="/info", error_prefix="/error"):
|
tim@3
|
1920 """Register a default set of OSC-address handlers with this Server:
|
tim@3
|
1921 - 'default' -> noCallback_handler
|
tim@3
|
1922 the given prefix is prepended to all other callbacks registered by this method:
|
tim@3
|
1923 - '<prefix><info_prefix' -> serverInfo_handler
|
tim@3
|
1924 - '<prefix><error_prefix> -> msgPrinter_handler
|
tim@3
|
1925 - '<prefix>/print' -> msgPrinter_handler
|
tim@3
|
1926 and, if the used Client supports it;
|
tim@3
|
1927 - '<prefix>/subscribe' -> subscription_handler
|
tim@3
|
1928 - '<prefix>/unsubscribe' -> subscription_handler
|
tim@3
|
1929
|
tim@3
|
1930 Note: the given 'error_prefix' argument is also set as default 'error_prefix' for error-messages
|
tim@3
|
1931 *sent from* this server. This is ok, because error-messages generally do not elicit a reply from the receiver.
|
tim@3
|
1932
|
tim@3
|
1933 To do this with the serverInfo-prefixes would be a bad idea, because if a request received on '/info' (for example)
|
tim@3
|
1934 would send replies to '/info', this could potentially cause a never-ending loop of messages!
|
tim@3
|
1935 Do *not* set the 'info_prefix' here (for incoming serverinfo requests) to the same value as given to
|
tim@3
|
1936 the setSrvInfoPrefix() method (for *replies* to incoming serverinfo requests).
|
tim@3
|
1937 For example, use '/info' for incoming requests, and '/inforeply' or '/serverinfo' or even just '/print' as the
|
tim@3
|
1938 info-reply prefix.
|
tim@3
|
1939 """
|
tim@3
|
1940 self.error_prefix = error_prefix
|
tim@3
|
1941 self.addMsgHandler('default', self.noCallback_handler)
|
tim@3
|
1942 self.addMsgHandler(prefix + info_prefix, self.serverInfo_handler)
|
tim@3
|
1943 self.addMsgHandler(prefix + error_prefix, self.msgPrinter_handler)
|
tim@3
|
1944 self.addMsgHandler(prefix + '/print', self.msgPrinter_handler)
|
tim@3
|
1945
|
tim@3
|
1946 if isinstance(self.client, OSCMultiClient):
|
tim@3
|
1947 self.addMsgHandler(prefix + '/subscribe', self.subscription_handler)
|
tim@3
|
1948 self.addMsgHandler(prefix + '/unsubscribe', self.subscription_handler)
|
tim@3
|
1949
|
tim@3
|
1950 def printErr(self, txt):
|
tim@3
|
1951 """Writes 'OSCServer: txt' to sys.stderr
|
tim@3
|
1952 """
|
tim@3
|
1953 sys.stderr.write("OSCServer: %s\n" % txt)
|
tim@3
|
1954
|
tim@3
|
1955 def sendOSCerror(self, txt, client_address):
|
tim@3
|
1956 """Sends 'txt', encapsulated in an OSCMessage to the default 'error_prefix' OSC-addres.
|
tim@3
|
1957 Message is sent to the given client_address, with the default 'return_port' overriding
|
tim@3
|
1958 the client_address' port, if defined.
|
tim@3
|
1959 """
|
tim@3
|
1960 lines = txt.split('\n')
|
tim@3
|
1961 if len(lines) == 1:
|
tim@3
|
1962 msg = OSCMessage(self.error_prefix)
|
tim@3
|
1963 msg.append(lines[0])
|
tim@3
|
1964 elif len(lines) > 1:
|
tim@3
|
1965 msg = OSCBundle(self.error_prefix)
|
tim@3
|
1966 for line in lines:
|
tim@3
|
1967 msg.append(line)
|
tim@3
|
1968 else:
|
tim@3
|
1969 return
|
tim@3
|
1970
|
tim@3
|
1971 if self.return_port:
|
tim@3
|
1972 client_address = (client_address[0], self.return_port)
|
tim@3
|
1973
|
tim@3
|
1974 self.client.sendto(msg, client_address)
|
tim@3
|
1975
|
tim@3
|
1976 def reportErr(self, txt, client_address):
|
tim@3
|
1977 """Writes 'OSCServer: txt' to sys.stderr
|
tim@3
|
1978 If self.error_prefix is defined, sends 'txt' as an OSC error-message to the client(s)
|
tim@3
|
1979 (see printErr() and sendOSCerror())
|
tim@3
|
1980 """
|
tim@3
|
1981 self.printErr(txt)
|
tim@3
|
1982
|
tim@3
|
1983 if len(self.error_prefix):
|
tim@3
|
1984 self.sendOSCerror(txt, client_address)
|
tim@3
|
1985
|
tim@3
|
1986 def sendOSCinfo(self, txt, client_address):
|
tim@3
|
1987 """Sends 'txt', encapsulated in an OSCMessage to the default 'info_prefix' OSC-addres.
|
tim@3
|
1988 Message is sent to the given client_address, with the default 'return_port' overriding
|
tim@3
|
1989 the client_address' port, if defined.
|
tim@3
|
1990 """
|
tim@3
|
1991 lines = txt.split('\n')
|
tim@3
|
1992 if len(lines) == 1:
|
tim@3
|
1993 msg = OSCMessage(self.info_prefix)
|
tim@3
|
1994 msg.append(lines[0])
|
tim@3
|
1995 elif len(lines) > 1:
|
tim@3
|
1996 msg = OSCBundle(self.info_prefix)
|
tim@3
|
1997 for line in lines:
|
tim@3
|
1998 msg.append(line)
|
tim@3
|
1999 else:
|
tim@3
|
2000 return
|
tim@3
|
2001
|
tim@3
|
2002 if self.return_port:
|
tim@3
|
2003 client_address = (client_address[0], self.return_port)
|
tim@3
|
2004
|
tim@3
|
2005 self.client.sendto(msg, client_address)
|
tim@3
|
2006
|
tim@3
|
2007 ###
|
tim@3
|
2008 # Message-Handler callback functions
|
tim@3
|
2009 ###
|
tim@3
|
2010
|
tim@3
|
2011 def handle_error(self, request, client_address):
|
tim@3
|
2012 """Handle an exception in the Server's callbacks gracefully.
|
tim@3
|
2013 Writes the error to sys.stderr and, if the error_prefix (see setSrvErrorPrefix()) is set,
|
tim@3
|
2014 sends the error-message as reply to the client
|
tim@3
|
2015 """
|
tim@3
|
2016 (e_type, e) = sys.exc_info()[:2]
|
tim@3
|
2017 if not str(e).endswith('[Errno 61] Connection refused'): # ADDED BY TIM
|
tim@3
|
2018 self.printErr("%s on request from %s: %s" % (e_type.__name__, getUrlStr(client_address), str(e)))
|
tim@3
|
2019
|
tim@3
|
2020 if self.print_tracebacks:
|
tim@3
|
2021 import traceback
|
tim@3
|
2022 traceback.print_exc() # XXX But this goes to stderr!
|
tim@3
|
2023
|
tim@3
|
2024 if len(self.error_prefix):
|
tim@3
|
2025 self.sendOSCerror("%s: %s" % (e_type.__name__, str(e)), client_address)
|
tim@3
|
2026
|
tim@3
|
2027 def noCallback_handler(self, addr, tags, data, client_address):
|
tim@3
|
2028 """Example handler for OSCMessages.
|
tim@3
|
2029 All registerd handlers must accept these three arguments:
|
tim@3
|
2030 - addr (string): The OSC-address pattern of the received Message
|
tim@3
|
2031 (the 'addr' string has already been matched against the handler's registerd OSC-address,
|
tim@3
|
2032 but may contain '*'s & such)
|
tim@3
|
2033 - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma)
|
tim@3
|
2034 - data (list): The OSCMessage's arguments
|
tim@3
|
2035 Note that len(tags) == len(data)
|
tim@3
|
2036 - client_address ((host, port) tuple): the host & port this message originated from.
|
tim@3
|
2037
|
tim@3
|
2038 a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle),
|
tim@3
|
2039 which then gets sent back to the client.
|
tim@3
|
2040
|
tim@3
|
2041 This handler prints a "No callback registered to handle ..." message.
|
tim@3
|
2042 Returns None
|
tim@3
|
2043 """
|
tim@3
|
2044 self.reportErr("No callback registered to handle OSC-address '%s'" % addr, client_address)
|
tim@3
|
2045
|
tim@3
|
2046 def msgPrinter_handler(self, addr, tags, data, client_address):
|
tim@3
|
2047 """Example handler for OSCMessages.
|
tim@3
|
2048 All registerd handlers must accept these three arguments:
|
tim@3
|
2049 - addr (string): The OSC-address pattern of the received Message
|
tim@3
|
2050 (the 'addr' string has already been matched against the handler's registerd OSC-address,
|
tim@3
|
2051 but may contain '*'s & such)
|
tim@3
|
2052 - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma)
|
tim@3
|
2053 - data (list): The OSCMessage's arguments
|
tim@3
|
2054 Note that len(tags) == len(data)
|
tim@3
|
2055 - client_address ((host, port) tuple): the host & port this message originated from.
|
tim@3
|
2056
|
tim@3
|
2057 a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle),
|
tim@3
|
2058 which then gets sent back to the client.
|
tim@3
|
2059
|
tim@3
|
2060 This handler prints the received message.
|
tim@3
|
2061 Returns None
|
tim@3
|
2062 """
|
tim@3
|
2063 txt = "OSCMessage '%s' from %s: " % (addr, getUrlStr(client_address))
|
tim@3
|
2064 txt += str(data)
|
tim@3
|
2065
|
tim@3
|
2066 self.printErr(txt) # strip trailing comma & space
|
tim@3
|
2067
|
tim@3
|
2068 def serverInfo_handler(self, addr, tags, data, client_address):
|
tim@3
|
2069 """Example handler for OSCMessages.
|
tim@3
|
2070 All registerd handlers must accept these three arguments:
|
tim@3
|
2071 - addr (string): The OSC-address pattern of the received Message
|
tim@3
|
2072 (the 'addr' string has already been matched against the handler's registerd OSC-address,
|
tim@3
|
2073 but may contain '*'s & such)
|
tim@3
|
2074 - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma)
|
tim@3
|
2075 - data (list): The OSCMessage's arguments
|
tim@3
|
2076 Note that len(tags) == len(data)
|
tim@3
|
2077 - client_address ((host, port) tuple): the host & port this message originated from.
|
tim@3
|
2078
|
tim@3
|
2079 a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle),
|
tim@3
|
2080 which then gets sent back to the client.
|
tim@3
|
2081
|
tim@3
|
2082 This handler returns a reply to the client, which can contain various bits of information
|
tim@3
|
2083 about this server, depending on the first argument of the received OSC-message:
|
tim@3
|
2084 - 'help' | 'info' : Reply contains server type & version info, plus a list of
|
tim@3
|
2085 available 'commands' understood by this handler
|
tim@3
|
2086 - 'list' | 'ls' : Reply is a bundle of 'address <string>' messages, listing the server's
|
tim@3
|
2087 OSC address-space.
|
tim@3
|
2088 - 'clients' | 'targets' : Reply is a bundle of 'target osc://<host>:<port>[<prefix>] [<filter>] [...]'
|
tim@3
|
2089 messages, listing the local Client-instance's subscribed remote clients.
|
tim@3
|
2090 """
|
tim@3
|
2091 if len(data) == 0:
|
tim@3
|
2092 return None
|
tim@3
|
2093
|
tim@3
|
2094 cmd = data.pop(0)
|
tim@3
|
2095
|
tim@3
|
2096 reply = None
|
tim@3
|
2097 if cmd in ('help', 'info'):
|
tim@3
|
2098 reply = OSCBundle(self.info_prefix)
|
tim@3
|
2099 reply.append(('server', str(self)))
|
tim@3
|
2100 reply.append(('info_command', "ls | list : list OSC address-space"))
|
tim@3
|
2101 reply.append(('info_command', "clients | targets : list subscribed clients"))
|
tim@3
|
2102 elif cmd in ('ls', 'list'):
|
tim@3
|
2103 reply = OSCBundle(self.info_prefix)
|
tim@3
|
2104 for addr in self.callbacks.keys():
|
tim@3
|
2105 reply.append(('address', addr))
|
tim@3
|
2106 elif cmd in ('clients', 'targets'):
|
tim@3
|
2107 if hasattr(self.client, 'getOSCTargetStrings'):
|
tim@3
|
2108 reply = OSCBundle(self.info_prefix)
|
tim@3
|
2109 for trg in self.client.getOSCTargetStrings():
|
tim@3
|
2110 reply.append(('target',) + trg)
|
tim@3
|
2111 else:
|
tim@3
|
2112 cli_addr = self.client.address()
|
tim@3
|
2113 if cli_addr:
|
tim@3
|
2114 reply = OSCMessage(self.info_prefix)
|
tim@3
|
2115 reply.append(('target', "osc://%s/" % getUrlStr(cli_addr)))
|
tim@3
|
2116 else:
|
tim@3
|
2117 self.reportErr("unrecognized command '%s' in /info request from osc://%s. Try 'help'" % (cmd, getUrlStr(client_address)), client_address)
|
tim@3
|
2118
|
tim@3
|
2119 return reply
|
tim@3
|
2120
|
tim@3
|
2121 def _subscribe(self, data, client_address):
|
tim@3
|
2122 """Handle the actual subscription. the provided 'data' is concatenated together to form a
|
tim@3
|
2123 '<host>:<port>[<prefix>] [<filter>] [...]' string, which is then passed to
|
tim@3
|
2124 parseUrlStr() & parseFilterStr() to actually retreive <host>, <port>, etc.
|
tim@3
|
2125
|
tim@3
|
2126 This 'long way 'round' approach (almost) guarantees that the subscription works,
|
tim@3
|
2127 regardless of how the bits of the <url> are encoded in 'data'.
|
tim@3
|
2128 """
|
tim@3
|
2129 url = ""
|
tim@3
|
2130 have_port = False
|
tim@3
|
2131 for item in data:
|
tim@3
|
2132 if (type(item) == types.IntType) and not have_port:
|
tim@3
|
2133 url += ":%d" % item
|
tim@3
|
2134 have_port = True
|
tim@3
|
2135 elif type(item) in types.StringTypes:
|
tim@3
|
2136 url += item
|
tim@3
|
2137
|
tim@3
|
2138 (addr, tail) = parseUrlStr(url)
|
tim@3
|
2139 (prefix, filters) = parseFilterStr(tail)
|
tim@3
|
2140
|
tim@3
|
2141 if addr != None:
|
tim@3
|
2142 (host, port) = addr
|
tim@3
|
2143 if not host:
|
tim@3
|
2144 host = client_address[0]
|
tim@3
|
2145 if not port:
|
tim@3
|
2146 port = client_address[1]
|
tim@3
|
2147 addr = (host, port)
|
tim@3
|
2148 else:
|
tim@3
|
2149 addr = client_address
|
tim@3
|
2150
|
tim@3
|
2151 self.client._setTarget(addr, prefix, filters)
|
tim@3
|
2152
|
tim@3
|
2153 trg = self.client.getOSCTargetStr(addr)
|
tim@3
|
2154 if trg[0] != None:
|
tim@3
|
2155 reply = OSCMessage(self.info_prefix)
|
tim@3
|
2156 reply.append(('target',) + trg)
|
tim@3
|
2157 return reply
|
tim@3
|
2158
|
tim@3
|
2159 def _unsubscribe(self, data, client_address):
|
tim@3
|
2160 """Handle the actual unsubscription. the provided 'data' is concatenated together to form a
|
tim@3
|
2161 '<host>:<port>[<prefix>]' string, which is then passed to
|
tim@3
|
2162 parseUrlStr() to actually retreive <host>, <port> & <prefix>.
|
tim@3
|
2163
|
tim@3
|
2164 This 'long way 'round' approach (almost) guarantees that the unsubscription works,
|
tim@3
|
2165 regardless of how the bits of the <url> are encoded in 'data'.
|
tim@3
|
2166 """
|
tim@3
|
2167 url = ""
|
tim@3
|
2168 have_port = False
|
tim@3
|
2169 for item in data:
|
tim@3
|
2170 if (type(item) == types.IntType) and not have_port:
|
tim@3
|
2171 url += ":%d" % item
|
tim@3
|
2172 have_port = True
|
tim@3
|
2173 elif type(item) in types.StringTypes:
|
tim@3
|
2174 url += item
|
tim@3
|
2175
|
tim@3
|
2176 (addr, _) = parseUrlStr(url)
|
tim@3
|
2177
|
tim@3
|
2178 if addr == None:
|
tim@3
|
2179 addr = client_address
|
tim@3
|
2180 else:
|
tim@3
|
2181 (host, port) = addr
|
tim@3
|
2182 if not host:
|
tim@3
|
2183 host = client_address[0]
|
tim@3
|
2184 if not port:
|
tim@3
|
2185 try:
|
tim@3
|
2186 (host, port) = self.client._searchHostAddr(host)
|
tim@3
|
2187 except NotSubscribedError:
|
tim@3
|
2188 port = client_address[1]
|
tim@3
|
2189
|
tim@3
|
2190 addr = (host, port)
|
tim@3
|
2191
|
tim@3
|
2192 try:
|
tim@3
|
2193 self.client._delTarget(addr)
|
tim@3
|
2194 except NotSubscribedError, e:
|
tim@3
|
2195 txt = "%s: %s" % (e.__class__.__name__, str(e))
|
tim@3
|
2196 self.printErr(txt)
|
tim@3
|
2197
|
tim@3
|
2198 reply = OSCMessage(self.error_prefix)
|
tim@3
|
2199 reply.append(txt)
|
tim@3
|
2200 return reply
|
tim@3
|
2201
|
tim@3
|
2202 def subscription_handler(self, addr, tags, data, client_address):
|
tim@3
|
2203 """Handle 'subscribe' / 'unsubscribe' requests from remote hosts,
|
tim@3
|
2204 if the local Client supports this (i.e. OSCMultiClient).
|
tim@3
|
2205
|
tim@3
|
2206 Supported commands:
|
tim@3
|
2207 - 'help' | 'info' : Reply contains server type & version info, plus a list of
|
tim@3
|
2208 available 'commands' understood by this handler
|
tim@3
|
2209 - 'list' | 'ls' : Reply is a bundle of 'target osc://<host>:<port>[<prefix>] [<filter>] [...]'
|
tim@3
|
2210 messages, listing the local Client-instance's subscribed remote clients.
|
tim@3
|
2211 - '[subscribe | listen | sendto | target] <url> [<filter> ...] : Subscribe remote client/server at <url>,
|
tim@3
|
2212 and/or set message-filters for messages being sent to the subscribed host, with the optional <filter>
|
tim@3
|
2213 arguments. Filters are given as OSC-addresses (or '*') prefixed by a '+' (send matching messages) or
|
tim@3
|
2214 a '-' (don't send matching messages). The wildcard '*', '+*' or '+/*' means 'send all' / 'filter none',
|
tim@3
|
2215 and '-*' or '-/*' means 'send none' / 'filter all' (which is not the same as unsubscribing!)
|
tim@3
|
2216 Reply is an OSCMessage with the (new) subscription; 'target osc://<host>:<port>[<prefix>] [<filter>] [...]'
|
tim@3
|
2217 - '[unsubscribe | silence | nosend | deltarget] <url> : Unsubscribe remote client/server at <url>
|
tim@3
|
2218 If the given <url> isn't subscribed, a NotSubscribedError-message is printed (and possibly sent)
|
tim@3
|
2219
|
tim@3
|
2220 The <url> given to the subscribe/unsubscribe handler should be of the form:
|
tim@3
|
2221 '[osc://][<host>][:<port>][<prefix>]', where any or all components can be omitted.
|
tim@3
|
2222
|
tim@3
|
2223 If <host> is not specified, the IP-address of the message's source is used.
|
tim@3
|
2224 If <port> is not specified, the <host> is first looked up in the list of subscribed hosts, and if found,
|
tim@3
|
2225 the associated port is used.
|
tim@3
|
2226 If <port> is not specified and <host> is not yet subscribed, the message's source-port is used.
|
tim@3
|
2227 If <prefix> is specified on subscription, <prefix> is prepended to the OSC-address of all messages
|
tim@3
|
2228 sent to the subscribed host.
|
tim@3
|
2229 If <prefix> is specified on unsubscription, the subscribed host is only unsubscribed if the host,
|
tim@3
|
2230 port and prefix all match the subscription.
|
tim@3
|
2231 If <prefix> is not specified on unsubscription, the subscribed host is unsubscribed if the host and port
|
tim@3
|
2232 match the subscription.
|
tim@3
|
2233 """
|
tim@3
|
2234 if not isinstance(self.client, OSCMultiClient):
|
tim@3
|
2235 raise OSCServerError("Local %s does not support subsctiptions or message-filtering" % self.client.__class__.__name__)
|
tim@3
|
2236
|
tim@3
|
2237 addr_cmd = addr.split('/')[-1]
|
tim@3
|
2238
|
tim@3
|
2239 if len(data):
|
tim@3
|
2240 if data[0] in ('help', 'info'):
|
tim@3
|
2241 reply = OSCBundle(self.info_prefix)
|
tim@3
|
2242 reply.append(('server', str(self)))
|
tim@3
|
2243 reply.append(('subscribe_command', "ls | list : list subscribed targets"))
|
tim@3
|
2244 reply.append(('subscribe_command', "[subscribe | listen | sendto | target] <url> [<filter> ...] : subscribe to messages, set filters"))
|
tim@3
|
2245 reply.append(('subscribe_command', "[unsubscribe | silence | nosend | deltarget] <url> : unsubscribe from messages"))
|
tim@3
|
2246 return reply
|
tim@3
|
2247
|
tim@3
|
2248 if data[0] in ('ls', 'list'):
|
tim@3
|
2249 reply = OSCBundle(self.info_prefix)
|
tim@3
|
2250 for trg in self.client.getOSCTargetStrings():
|
tim@3
|
2251 reply.append(('target',) + trg)
|
tim@3
|
2252 return reply
|
tim@3
|
2253
|
tim@3
|
2254 if data[0] in ('subscribe', 'listen', 'sendto', 'target'):
|
tim@3
|
2255 return self._subscribe(data[1:], client_address)
|
tim@3
|
2256
|
tim@3
|
2257 if data[0] in ('unsubscribe', 'silence', 'nosend', 'deltarget'):
|
tim@3
|
2258 return self._unsubscribe(data[1:], client_address)
|
tim@3
|
2259
|
tim@3
|
2260 if addr_cmd in ('subscribe', 'listen', 'sendto', 'target'):
|
tim@3
|
2261 return self._subscribe(data, client_address)
|
tim@3
|
2262
|
tim@3
|
2263 if addr_cmd in ('unsubscribe', 'silence', 'nosend', 'deltarget'):
|
tim@3
|
2264 return self._unsubscribe(data, client_address)
|
tim@3
|
2265
|
tim@3
|
2266 class ForkingOSCServer(ForkingMixIn, OSCServer):
|
tim@3
|
2267 """An Asynchronous OSCServer.
|
tim@3
|
2268 This server forks a new process to handle each incoming request.
|
tim@3
|
2269 """
|
tim@3
|
2270 # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer
|
tim@3
|
2271 RequestHandlerClass = ThreadingOSCRequestHandler
|
tim@3
|
2272
|
tim@3
|
2273 class ThreadingOSCServer(ThreadingMixIn, OSCServer):
|
tim@3
|
2274 """An Asynchronous OSCServer.
|
tim@3
|
2275 This server starts a new thread to handle each incoming request.
|
tim@3
|
2276 """
|
tim@3
|
2277 # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer
|
tim@3
|
2278 RequestHandlerClass = ThreadingOSCRequestHandler
|
tim@3
|
2279
|
tim@3
|
2280 ######
|
tim@3
|
2281 #
|
tim@3
|
2282 # OSCError classes
|
tim@3
|
2283 #
|
tim@3
|
2284 ######
|
tim@3
|
2285
|
tim@3
|
2286 class OSCError(Exception):
|
tim@3
|
2287 """Base Class for all OSC-related errors
|
tim@3
|
2288 """
|
tim@3
|
2289 def __init__(self, message):
|
tim@3
|
2290 self.message = message
|
tim@3
|
2291
|
tim@3
|
2292 def __str__(self):
|
tim@3
|
2293 return self.message
|
tim@3
|
2294
|
tim@3
|
2295 class OSCClientError(OSCError):
|
tim@3
|
2296 """Class for all OSCClient errors
|
tim@3
|
2297 """
|
tim@3
|
2298 pass
|
tim@3
|
2299
|
tim@3
|
2300 class OSCServerError(OSCError):
|
tim@3
|
2301 """Class for all OSCServer errors
|
tim@3
|
2302 """
|
tim@3
|
2303 pass
|
tim@3
|
2304
|
tim@3
|
2305 class NoCallbackError(OSCServerError):
|
tim@3
|
2306 """This error is raised (by an OSCServer) when an OSCMessage with an 'unmatched' address-pattern
|
tim@3
|
2307 is received, and no 'default' handler is registered.
|
tim@3
|
2308 """
|
tim@3
|
2309 def __init__(self, pattern):
|
tim@3
|
2310 """The specified 'pattern' should be the OSC-address of the 'unmatched' message causing the error to be raised.
|
tim@3
|
2311 """
|
tim@3
|
2312 self.message = "No callback registered to handle OSC-address '%s'" % pattern
|
tim@3
|
2313
|
tim@3
|
2314 class NotSubscribedError(OSCClientError):
|
tim@3
|
2315 """This error is raised (by an OSCMultiClient) when an attempt is made to unsubscribe a host
|
tim@3
|
2316 that isn't subscribed.
|
tim@3
|
2317 """
|
tim@3
|
2318 def __init__(self, addr, prefix=None):
|
tim@3
|
2319 if prefix:
|
tim@3
|
2320 url = getUrlStr(addr, prefix)
|
tim@3
|
2321 else:
|
tim@3
|
2322 url = getUrlStr(addr, '')
|
tim@3
|
2323
|
tim@3
|
2324 self.message = "Target osc://%s is not subscribed" % url
|
tim@3
|
2325
|
tim@3
|
2326 ######
|
tim@3
|
2327 #
|
tim@3
|
2328 # Testing Program
|
tim@3
|
2329 #
|
tim@3
|
2330 ######
|
tim@3
|
2331
|
tim@3
|
2332
|
tim@3
|
2333 if __name__ == "__main__":
|
tim@3
|
2334 import optparse
|
tim@3
|
2335
|
tim@3
|
2336 default_port = 2222
|
tim@3
|
2337
|
tim@3
|
2338 # define command-line options
|
tim@3
|
2339 op = optparse.OptionParser(description="OSC.py OpenSoundControl-for-Python Test Program")
|
tim@3
|
2340 op.add_option("-l", "--listen", dest="listen",
|
tim@3
|
2341 help="listen on given host[:port]. default = '0.0.0.0:%d'" % default_port)
|
tim@3
|
2342 op.add_option("-s", "--sendto", dest="sendto",
|
tim@3
|
2343 help="send to given host[:port]. default = '127.0.0.1:%d'" % default_port)
|
tim@3
|
2344 op.add_option("-t", "--threading", action="store_true", dest="threading",
|
tim@3
|
2345 help="Test ThreadingOSCServer")
|
tim@3
|
2346 op.add_option("-f", "--forking", action="store_true", dest="forking",
|
tim@3
|
2347 help="Test ForkingOSCServer")
|
tim@3
|
2348 op.add_option("-u", "--usage", action="help", help="show this help message and exit")
|
tim@3
|
2349
|
tim@3
|
2350 op.set_defaults(listen=":%d" % default_port)
|
tim@3
|
2351 op.set_defaults(sendto="")
|
tim@3
|
2352 op.set_defaults(threading=False)
|
tim@3
|
2353 op.set_defaults(forking=False)
|
tim@3
|
2354
|
tim@3
|
2355 # Parse args
|
tim@3
|
2356 (opts, args) = op.parse_args()
|
tim@3
|
2357
|
tim@3
|
2358 addr, server_prefix = parseUrlStr(opts.listen)
|
tim@3
|
2359 if addr != None and addr[0] != None:
|
tim@3
|
2360 if addr[1] != None:
|
tim@3
|
2361 listen_address = addr
|
tim@3
|
2362 else:
|
tim@3
|
2363 listen_address = (addr[0], default_port)
|
tim@3
|
2364 else:
|
tim@3
|
2365 listen_address = ('', default_port)
|
tim@3
|
2366
|
tim@3
|
2367 targets = {}
|
tim@3
|
2368 for trg in opts.sendto.split(','):
|
tim@3
|
2369 (addr, prefix) = parseUrlStr(trg)
|
tim@3
|
2370 if len(prefix):
|
tim@3
|
2371 (prefix, filters) = parseFilterStr(prefix)
|
tim@3
|
2372 else:
|
tim@3
|
2373 filters = {}
|
tim@3
|
2374
|
tim@3
|
2375 if addr != None:
|
tim@3
|
2376 if addr[1] != None:
|
tim@3
|
2377 targets[addr] = [prefix, filters]
|
tim@3
|
2378 else:
|
tim@3
|
2379 targets[(addr[0], listen_address[1])] = [prefix, filters]
|
tim@3
|
2380 elif len(prefix) or len(filters):
|
tim@3
|
2381 targets[listen_address] = [prefix, filters]
|
tim@3
|
2382
|
tim@3
|
2383 welcome = "Welcome to the OSC testing program."
|
tim@3
|
2384 print welcome
|
tim@3
|
2385 hexDump(welcome)
|
tim@3
|
2386 print
|
tim@3
|
2387 message = OSCMessage()
|
tim@3
|
2388 message.setAddress("/print")
|
tim@3
|
2389 message.append(44)
|
tim@3
|
2390 message.append(11)
|
tim@3
|
2391 message.append(4.5)
|
tim@3
|
2392 message.append("the white cliffs of dover")
|
tim@3
|
2393
|
tim@3
|
2394 print message
|
tim@3
|
2395 hexDump(message.getBinary())
|
tim@3
|
2396
|
tim@3
|
2397 print "\nMaking and unmaking a message.."
|
tim@3
|
2398
|
tim@3
|
2399 strings = OSCMessage("/prin{ce,t}")
|
tim@3
|
2400 strings.append("Mary had a little lamb")
|
tim@3
|
2401 strings.append("its fleece was white as snow")
|
tim@3
|
2402 strings.append("and everywhere that Mary went,")
|
tim@3
|
2403 strings.append("the lamb was sure to go.")
|
tim@3
|
2404 strings.append(14.5)
|
tim@3
|
2405 strings.append(14.5)
|
tim@3
|
2406 strings.append(-400)
|
tim@3
|
2407
|
tim@3
|
2408 raw = strings.getBinary()
|
tim@3
|
2409
|
tim@3
|
2410 print strings
|
tim@3
|
2411 hexDump(raw)
|
tim@3
|
2412
|
tim@3
|
2413 print "Retrieving arguments..."
|
tim@3
|
2414 data = raw
|
tim@3
|
2415 for i in range(6):
|
tim@3
|
2416 text, data = _readString(data)
|
tim@3
|
2417 print text
|
tim@3
|
2418
|
tim@3
|
2419 number, data = _readFloat(data)
|
tim@3
|
2420 print number
|
tim@3
|
2421
|
tim@3
|
2422 number, data = _readFloat(data)
|
tim@3
|
2423 print number
|
tim@3
|
2424
|
tim@3
|
2425 number, data = _readInt(data)
|
tim@3
|
2426 print number
|
tim@3
|
2427
|
tim@3
|
2428 print decodeOSC(raw)
|
tim@3
|
2429
|
tim@3
|
2430 print "\nTesting Blob types."
|
tim@3
|
2431
|
tim@3
|
2432 blob = OSCMessage("/pri*")
|
tim@3
|
2433 blob.append("","b")
|
tim@3
|
2434 blob.append("b","b")
|
tim@3
|
2435 blob.append("bl","b")
|
tim@3
|
2436 blob.append("blo","b")
|
tim@3
|
2437 blob.append("blob","b")
|
tim@3
|
2438 blob.append("blobs","b")
|
tim@3
|
2439 blob.append(42)
|
tim@3
|
2440
|
tim@3
|
2441 print blob
|
tim@3
|
2442 hexDump(blob.getBinary())
|
tim@3
|
2443
|
tim@3
|
2444 print1 = OSCMessage()
|
tim@3
|
2445 print1.setAddress("/print")
|
tim@3
|
2446 print1.append("Hey man, that's cool.")
|
tim@3
|
2447 print1.append(42)
|
tim@3
|
2448 print1.append(3.1415926)
|
tim@3
|
2449
|
tim@3
|
2450 print "\nTesting OSCBundle"
|
tim@3
|
2451
|
tim@3
|
2452 bundle = OSCBundle()
|
tim@3
|
2453 bundle.append(print1)
|
tim@3
|
2454 bundle.append({'addr':"/print", 'args':["bundled messages:", 2]})
|
tim@3
|
2455 bundle.setAddress("/*print")
|
tim@3
|
2456 bundle.append(("no,", 3, "actually."))
|
tim@3
|
2457
|
tim@3
|
2458 print bundle
|
tim@3
|
2459 hexDump(bundle.getBinary())
|
tim@3
|
2460
|
tim@3
|
2461 # Instantiate OSCClient
|
tim@3
|
2462 print "\nInstantiating OSCClient:"
|
tim@3
|
2463 if len(targets):
|
tim@3
|
2464 c = OSCMultiClient()
|
tim@3
|
2465 c.updateOSCTargets(targets)
|
tim@3
|
2466 else:
|
tim@3
|
2467 c = OSCClient()
|
tim@3
|
2468 c.connect(listen_address) # connect back to our OSCServer
|
tim@3
|
2469
|
tim@3
|
2470 print c
|
tim@3
|
2471 if hasattr(c, 'getOSCTargetStrings'):
|
tim@3
|
2472 print "Sending to:"
|
tim@3
|
2473 for (trg, filterstrings) in c.getOSCTargetStrings():
|
tim@3
|
2474 out = trg
|
tim@3
|
2475 for fs in filterstrings:
|
tim@3
|
2476 out += " %s" % fs
|
tim@3
|
2477
|
tim@3
|
2478 print out
|
tim@3
|
2479
|
tim@3
|
2480 # Now an OSCServer...
|
tim@3
|
2481 print "\nInstantiating OSCServer:"
|
tim@3
|
2482
|
tim@3
|
2483 # define a message-handler function for the server to call.
|
tim@3
|
2484 def printing_handler(addr, tags, stuff, source):
|
tim@3
|
2485 msg_string = "%s [%s] %s" % (addr, tags, str(stuff))
|
tim@3
|
2486 sys.stdout.write("OSCServer Got: '%s' from %s\n" % (msg_string, getUrlStr(source)))
|
tim@3
|
2487
|
tim@3
|
2488 # send a reply to the client.
|
tim@3
|
2489 msg = OSCMessage("/printed")
|
tim@3
|
2490 msg.append(msg_string)
|
tim@3
|
2491 return msg
|
tim@3
|
2492
|
tim@3
|
2493 if opts.threading:
|
tim@3
|
2494 s = ThreadingOSCServer(listen_address, c, return_port=listen_address[1])
|
tim@3
|
2495 elif opts.forking:
|
tim@3
|
2496 s = ForkingOSCServer(listen_address, c, return_port=listen_address[1])
|
tim@3
|
2497 else:
|
tim@3
|
2498 s = OSCServer(listen_address, c, return_port=listen_address[1])
|
tim@3
|
2499
|
tim@3
|
2500 print s
|
tim@3
|
2501
|
tim@3
|
2502 # Set Server to return errors as OSCMessages to "/error"
|
tim@3
|
2503 s.setSrvErrorPrefix("/error")
|
tim@3
|
2504 # Set Server to reply to server-info requests with OSCMessages to "/serverinfo"
|
tim@3
|
2505 s.setSrvInfoPrefix("/serverinfo")
|
tim@3
|
2506
|
tim@3
|
2507 # this registers a 'default' handler (for unmatched messages),
|
tim@3
|
2508 # an /'error' handler, an '/info' handler.
|
tim@3
|
2509 # And, if the client supports it, a '/subscribe' & '/unsubscribe' handler
|
tim@3
|
2510 s.addDefaultHandlers()
|
tim@3
|
2511
|
tim@3
|
2512 s.addMsgHandler("/print", printing_handler)
|
tim@3
|
2513
|
tim@3
|
2514 # if client & server are bound to 'localhost', server replies return to itself!
|
tim@3
|
2515 s.addMsgHandler("/printed", s.msgPrinter_handler)
|
tim@3
|
2516 s.addMsgHandler("/serverinfo", s.msgPrinter_handler)
|
tim@3
|
2517
|
tim@3
|
2518 print "Registered Callback-functions:"
|
tim@3
|
2519 for addr in s.getOSCAddressSpace():
|
tim@3
|
2520 print addr
|
tim@3
|
2521
|
tim@3
|
2522 print "\nStarting OSCServer. Use ctrl-C to quit."
|
tim@3
|
2523 st = threading.Thread(target=s.serve_forever)
|
tim@3
|
2524 st.start()
|
tim@3
|
2525
|
tim@3
|
2526 if hasattr(c, 'targets') and listen_address not in c.targets.keys():
|
tim@3
|
2527 print "\nSubscribing local Server to local Client"
|
tim@3
|
2528 c2 = OSCClient()
|
tim@3
|
2529 c2.connect(listen_address)
|
tim@3
|
2530 subreq = OSCMessage("/subscribe")
|
tim@3
|
2531 subreq.append(listen_address)
|
tim@3
|
2532
|
tim@3
|
2533 print "sending: ", subreq
|
tim@3
|
2534 c2.send(subreq)
|
tim@3
|
2535 c2.close()
|
tim@3
|
2536
|
tim@3
|
2537 time.sleep(0.1)
|
tim@3
|
2538
|
tim@3
|
2539 print "\nRequesting OSC-address-space and subscribed clients from OSCServer"
|
tim@3
|
2540 inforeq = OSCMessage("/info")
|
tim@3
|
2541 for cmd in ("info", "list", "clients"):
|
tim@3
|
2542 inforeq.clearData()
|
tim@3
|
2543 inforeq.append(cmd)
|
tim@3
|
2544
|
tim@3
|
2545 print "sending: ", inforeq
|
tim@3
|
2546 c.send(inforeq)
|
tim@3
|
2547
|
tim@3
|
2548 time.sleep(0.1)
|
tim@3
|
2549
|
tim@3
|
2550 print2 = print1.copy()
|
tim@3
|
2551 print2.setAddress('/noprint')
|
tim@3
|
2552
|
tim@3
|
2553 print "\nSending Messages"
|
tim@3
|
2554
|
tim@3
|
2555 for m in (message, print1, print2, strings, bundle):
|
tim@3
|
2556 print "sending: ", m
|
tim@3
|
2557 c.send(m)
|
tim@3
|
2558
|
tim@3
|
2559 time.sleep(0.1)
|
tim@3
|
2560
|
tim@3
|
2561 print "\nThe next message's address will match both the '/print' and '/printed' handlers..."
|
tim@3
|
2562 print "sending: ", blob
|
tim@3
|
2563 c.send(blob)
|
tim@3
|
2564
|
tim@3
|
2565 time.sleep(0.1)
|
tim@3
|
2566
|
tim@3
|
2567 print "\nBundles can be given a timestamp.\nThe receiving server should 'hold' the bundle until its time has come"
|
tim@3
|
2568
|
tim@3
|
2569 waitbundle = OSCBundle("/print")
|
tim@3
|
2570 waitbundle.setTimeTag(time.time() + 5)
|
tim@3
|
2571 if s.__class__ == OSCServer:
|
tim@3
|
2572 waitbundle.append("Note how the (single-thread) OSCServer blocks while holding this bundle")
|
tim@3
|
2573 else:
|
tim@3
|
2574 waitbundle.append("Note how the %s does not block while holding this bundle" % s.__class__.__name__)
|
tim@3
|
2575
|
tim@3
|
2576 print "Set timetag 5 s into the future"
|
tim@3
|
2577 print "sending: ", waitbundle
|
tim@3
|
2578 c.send(waitbundle)
|
tim@3
|
2579
|
tim@3
|
2580 time.sleep(0.1)
|
tim@3
|
2581
|
tim@3
|
2582 print "Recursing bundles, with timetags set to 10 s [25 s, 20 s, 10 s]"
|
tim@3
|
2583 bb = OSCBundle("/print")
|
tim@3
|
2584 bb.setTimeTag(time.time() + 10)
|
tim@3
|
2585
|
tim@3
|
2586 b = OSCBundle("/print")
|
tim@3
|
2587 b.setTimeTag(time.time() + 25)
|
tim@3
|
2588 b.append("held for 25 sec")
|
tim@3
|
2589 bb.append(b)
|
tim@3
|
2590
|
tim@3
|
2591 b.clearData()
|
tim@3
|
2592 b.setTimeTag(time.time() + 20)
|
tim@3
|
2593 b.append("held for 20 sec")
|
tim@3
|
2594 bb.append(b)
|
tim@3
|
2595
|
tim@3
|
2596 b.clearData()
|
tim@3
|
2597 b.setTimeTag(time.time() + 15)
|
tim@3
|
2598 b.append("held for 15 sec")
|
tim@3
|
2599 bb.append(b)
|
tim@3
|
2600
|
tim@3
|
2601 if s.__class__ == OSCServer:
|
tim@3
|
2602 bb.append("Note how the (single-thread) OSCServer handles the bundle's contents in order of appearance")
|
tim@3
|
2603 else:
|
tim@3
|
2604 bb.append("Note how the %s handles the sub-bundles in the order dictated by their timestamps" % s.__class__.__name__)
|
tim@3
|
2605 bb.append("Each bundle's contents, however, are processed in random order (dictated by the kernel's threading)")
|
tim@3
|
2606
|
tim@3
|
2607 print "sending: ", bb
|
tim@3
|
2608 c.send(bb)
|
tim@3
|
2609
|
tim@3
|
2610 time.sleep(0.1)
|
tim@3
|
2611
|
tim@3
|
2612 print "\nMessages sent!"
|
tim@3
|
2613
|
tim@3
|
2614 print "\nWaiting for OSCServer. Use ctrl-C to quit.\n"
|
tim@3
|
2615
|
tim@3
|
2616 try:
|
tim@3
|
2617 while True:
|
tim@3
|
2618 time.sleep(30)
|
tim@3
|
2619
|
tim@3
|
2620 except KeyboardInterrupt:
|
tim@3
|
2621 print "\nClosing OSCServer."
|
tim@3
|
2622 s.close()
|
tim@3
|
2623 print "Waiting for Server-thread to finish"
|
tim@3
|
2624 st.join()
|
tim@3
|
2625 print "Closing OSCClient"
|
tim@3
|
2626 c.close()
|
tim@3
|
2627 print "Done"
|
tim@3
|
2628
|
tim@3
|
2629 sys.exit(0)
|