commit a5d76ba126a1c6e741355127ca3a81db6d0566d0 Author: Sam Neurohack Date: Wed Jan 6 03:32:39 2021 +0100 not finished commit diff --git a/OSC3.py b/OSC3.py new file mode 100755 index 0000000..52a38f5 --- /dev/null +++ b/OSC3.py @@ -0,0 +1,2874 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +March 2015: + Python 3 version tested in Blender and simpleOSC with twisted + +This module contains an OpenSoundControl implementation (in Pure Python), based +(somewhat) on the good old 'SimpleOSC' implementation by Daniel Holth & Clinton +McChesney. + +This implementation is intended to still be 'simple' to the user, but much more +complete (with OSCServer & OSCClient classes) and much more powerful (the +OSCMultiClient supports subscriptions & message-filtering, OSCMessage & +OSCBundle are now proper container-types) + +=============================================================================== +OpenSoundControl +=============================================================================== + +OpenSoundControl is a network-protocol for sending (small) packets of addressed +data over network sockets. This OSC-implementation supports the classical +UDP/IP protocol for sending and receiving packets but provides as well support +for TCP/IP streaming, whereas the message size is prepended as int32 (big +endian) before each message/packet. + +OSC-packets come in two kinds: + + - OSC-messages consist of an 'address'-string (not to be confused with a + (host:port) network-address!), followed by a string of 'typetags' + associated with the message's arguments (ie. 'payload'), and finally the + arguments themselves, encoded in an OSC-specific way. The OSCMessage class + makes it easy to create & manipulate OSC-messages of this kind in a + 'pythonesque' way (that is, OSCMessage-objects behave a lot like lists) + + - OSC-bundles are a special type of OSC-message containing only + OSC-messages as 'payload'. Recursively. (meaning; an OSC-bundle could + contain other OSC-bundles, containing OSC-bundles etc.) + +OSC-bundles start with the special keyword '#bundle' and do not have an +OSC-address (but the OSC-messages a bundle contains will have OSC-addresses!). +Also, an OSC-bundle can have a timetag, essentially telling the receiving +server to 'hold' the bundle until the specified time. The OSCBundle class +allows easy cration & manipulation of OSC-bundles. + +For further information see also http://opensoundcontrol.org/spec-1_0 + +------------------------------------------------------------------------------- + +To send OSC-messages, you need an OSCClient, and to receive OSC-messages you +need an OSCServer. + +The OSCClient uses an 'AF_INET / SOCK_DGRAM' type socket (see the 'socket' +module) to send binary representations of OSC-messages to a remote host:port +address. + +The OSCServer listens on an 'AF_INET / SOCK_DGRAM' type socket bound to a local +port, and handles incoming requests. Either one-after-the-other (OSCServer) or +in a multi-threaded / multi-process fashion (ThreadingOSCServer/ +ForkingOSCServer). If the Server has a callback-function (a.k.a. handler) +registered to 'deal with' (i.e. handle) the received message's OSC-address, +that function is called, passing it the (decoded) message. + +The different OSCServers implemented here all support the (recursive) un- +bundling of OSC-bundles, and OSC-bundle timetags. + +In fact, this implementation supports: + + - OSC-messages with 'i' (int32), 'f' (float32), 'd' (double), 's' (string) and + 'b' (blob / binary data) types + - OSC-bundles, including timetag-support + - OSC-address patterns including '*', '?', '{,}' and '[]' wildcards. + +(please *do* read the OSC-spec! http://opensoundcontrol.org/spec-1_0 it +explains what these things mean.) + +In addition, the OSCMultiClient supports: + - Sending a specific OSC-message to multiple remote servers + - Remote server subscription / unsubscription (through OSC-messages, of course) + - Message-address filtering. + +------------------------------------------------------------------------------- +SimpleOSC: + Copyright (c) Daniel Holth & Clinton McChesney. +pyOSC: + Copyright (c) 2008-2010, Artem Baguinski et al., Stock, V2_Lab, Rotterdam, Netherlands. +Streaming support (OSC over TCP): + Copyright (c) 2010 Uli Franke , Weiss Engineering, Uster, Switzerland. + +------------------------------------------------------------------------------- +Changelog: +------------------------------------------------------------------------------- +v0.3.0 - 27 Dec. 2007 + Started out to extend the 'SimpleOSC' implementation (v0.2.3) by Daniel Holth & Clinton McChesney. + Rewrote OSCMessage + Added OSCBundle + +v0.3.1 - 3 Jan. 2008 + Added OSClient + Added OSCRequestHandler, loosely based on the original CallbackManager + Added OSCServer + Removed original CallbackManager + Adapted testing-script (the 'if __name__ == "__main__":' block at the end) to use new Server & Client + +v0.3.2 - 5 Jan. 2008 + Added 'container-type emulation' methods (getitem(), setitem(), __iter__() & friends) to OSCMessage + Added ThreadingOSCServer & ForkingOSCServer + - 6 Jan. 2008 + Added OSCMultiClient + Added command-line options to testing-script (try 'python OSC.py --help') + +v0.3.3 - 9 Jan. 2008 + Added OSC-timetag support to OSCBundle & OSCRequestHandler + Added ThreadingOSCRequestHandler + +v0.3.4 - 13 Jan. 2008 + Added message-filtering to OSCMultiClient + Added subscription-handler to OSCServer + Added support fon numpy/scipy int & float types. (these get converted to 'standard' 32-bit OSC ints / floats!) + Cleaned-up and added more Docstrings + +v0.3.5 - 14 aug. 2008 + Added OSCServer.reportErr(...) method + +v0.3.6 - 19 April 2010 + Added Streaming support (OSC over TCP) + Updated documentation + Moved pattern matching stuff into separate class (OSCAddressSpace) to + facilitate implementation of different server and client architectures. + Callbacks feature now a context (object oriented) but dynamic function + inspection keeps the code backward compatible + Moved testing code into separate testbench (testbench.py) + +----------------- +Original Comments +----------------- +> Open SoundControl for Python +> Copyright (C) 2002 Daniel Holth, Clinton McChesney +> +> This library is free software; you can redistribute it and/or modify it under +> the terms of the GNU Lesser General Public License as published by the Free +> Software Foundation; either version 2.1 of the License, or (at your option) any +> later version. +> +> This library is distributed in the hope that it will be useful, but WITHOUT ANY +> WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +> PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +> details. +> +> You should have received a copy of the GNU Lesser General Public License along +> with this library; if not, write to the Free Software Foundation, Inc., 59 +> Temple Place, Suite 330, Boston, MA 02111-1307 USA +> +> For questions regarding this module contact Daniel Holth +> or visit http://www.stetson.edu/~ProctoLogic/ +> +> Changelog: +> 15 Nov. 2001: +> Removed dependency on Python 2.0 features. +> - dwh +> 13 Feb. 2002: +> Added a generic callback handler. +> - dwh +""" + +import math, re, socket, select, string, struct, sys, threading, time, types, array, errno, inspect +from socketserver import UDPServer, DatagramRequestHandler, ForkingMixIn, ThreadingMixIn, StreamRequestHandler, TCPServer +from contextlib import closing + +global version +version = ("0.3","6", "$Rev: 6382 $"[6:-2]) + +global FloatTypes +FloatTypes = [float] + +global IntTypes +IntTypes = [int] + +global NTP_epoch +from calendar import timegm +NTP_epoch = timegm((1900,1,1,0,0,0)) # NTP time started in 1 Jan 1900 +del timegm + +global NTP_units_per_second +NTP_units_per_second = 0x100000000 # about 232 picoseconds + + +## +# numpy/scipy support: +## + +try: + from numpy import typeDict + + for ftype in ['float32', 'float64', 'float128']: + try: + FloatTypes.append(typeDict[ftype]) + except KeyError: + pass + + for itype in ['int8', 'int16', 'int32', 'int64']: + try: + IntTypes.append(typeDict[itype]) + IntTypes.append(typeDict['u' + itype]) + except KeyError: + pass + + # thanks for those... + del typeDict, ftype, itype + +except ImportError: + pass + +###### +# +# OSCMessage classes +# +###### + +class OSCMessage(object): + """ Builds typetagged OSC messages. + + OSCMessage objects are container objects for building OSC-messages. + On the 'front' end, they behave much like list-objects, and on the 'back' end + they generate a binary representation of the message, which can be sent over a network socket. + OSC-messages consist of an 'address'-string (not to be confused with a (host, port) IP-address!), + followed by a string of 'typetags' associated with the message's arguments (ie. 'payload'), + and finally the arguments themselves, encoded in an OSC-specific way. + + On the Python end, OSCMessage are lists of arguments, prepended by the message's address. + The message contents can be manipulated much like a list: + >>> msg = OSCMessage("/my/osc/address") + >>> msg.append('something') + >>> msg.insert(0, 'something else') + >>> msg[1] = 'entirely' + >>> msg.extend([1,2,3.]) + >>> msg += [4, 5, 6.] + >>> del msg[3:6] + >>> msg.pop(-2) + 5 + >>> print msg + /my/osc/address ['something else', 'entirely', 1, 6.0] + + OSCMessages can be concatenated with the + operator. In this case, the resulting OSCMessage + inherits its address from the left-hand operand. The right-hand operand's address is ignored. + To construct an 'OSC-bundle' from multiple OSCMessage, see OSCBundle! + + Additional methods exist for retreiving typetags or manipulating items as (typetag, value) tuples. + """ + def __init__(self, address=""): + """Instantiate a new OSCMessage. + The OSC-address can be specified with the 'address' argument + """ + self.clear(address) + + def setAddress(self, address): + """Set or change the OSC-address + """ + self.address = address + + def clear(self, address=""): + """Clear (or set a new) OSC-address and clear any arguments appended so far + """ + self.address = address + self.clearData() + + def clearData(self): + """Clear any arguments appended so far + """ + self.typetags = "," + self.message = b"" + + def append(self, argument, typehint=None): + """Appends data to the message, updating the typetags based on + the argument's type. If the argument is a blob (counted + string) pass in 'b' as typehint. + 'argument' may also be a list or tuple, in which case its elements + will get appended one-by-one, all using the provided typehint + """ + if isinstance(argument,dict): + argument = list(argument.items()) + elif isinstance(argument, OSCMessage): + raise TypeError("Can only append 'OSCMessage' to 'OSCBundle'") + + if hasattr(argument, '__iter__') and not type(argument) in (str,bytes): + for arg in argument: + self.append(arg, typehint) + + return + + if typehint == 'b': + binary = OSCBlob(argument) + tag = 'b' + elif typehint == 't': + binary = OSCTimeTag(argument) + tag = 't' + else: + tag, binary = OSCArgument(argument, typehint) + + self.typetags += tag + self.message += binary + + def getBinary(self): + """Returns the binary representation of the message + """ + binary = OSCString(self.address) + binary += OSCString(self.typetags) + binary += self.message + + return binary + + def __repr__(self): + """Returns a string containing the decode Message + """ + return str(decodeOSC(self.getBinary())) + + def __str__(self): + """Returns the Message's address and contents as a string. + """ + return "%s %s" % (self.address, str(list(self.values()))) + + def __len__(self): + """Returns the number of arguments appended so far + """ + return (len(self.typetags) - 1) + + def __eq__(self, other): + """Return True if two OSCMessages have the same address & content + """ + if not isinstance(other, self.__class__): + return False + + return (self.address == other.address) and (self.typetags == other.typetags) and (self.message == other.message) + + def __ne__(self, other): + """Return (not self.__eq__(other)) + """ + return not self.__eq__(other) + + def __add__(self, values): + """Returns a copy of self, with the contents of 'values' appended + (see the 'extend()' method, below) + """ + msg = self.copy() + msg.extend(values) + return msg + + def __iadd__(self, values): + """Appends the contents of 'values' + (equivalent to 'extend()', below) + Returns self + """ + self.extend(values) + return self + + def __radd__(self, values): + """Appends the contents of this OSCMessage to 'values' + Returns the extended 'values' (list or tuple) + """ + out = list(values) + out.extend(list(self.values())) + + if isinstance(values,tuple): + return tuple(out) + + return out + + def _reencode(self, items): + """Erase & rebuild the OSCMessage contents from the given + list of (typehint, value) tuples""" + self.clearData() + for item in items: + self.append(item[1], item[0]) + + def values(self): + """Returns a list of the arguments appended so far + """ + return decodeOSC(self.getBinary())[2:] + + def tags(self): + """Returns a list of typetags of the appended arguments + """ + return list(self.typetags.lstrip(',')) + + def items(self): + """Returns a list of (typetag, value) tuples for + the arguments appended so far + """ + out = [] + values = list(self.values()) + typetags = self.tags() + for i in range(len(values)): + out.append((typetags[i], values[i])) + + return out + + def __contains__(self, val): + """Test if the given value appears in the OSCMessage's arguments + """ + return (val in list(self.values())) + + def __getitem__(self, i): + """Returns the indicated argument (or slice) + """ + return list(self.values())[i] + + def __delitem__(self, i): + """Removes the indicated argument (or slice) + """ + items = list(self.items()) + del items[i] + + self._reencode(items) + + def _buildItemList(self, values, typehint=None): + if isinstance(values, OSCMessage): + items = list(values.items()) + elif isinstance(values,list): + items = [] + for val in values: + if isinstance(val,tuple): + items.append(val[:2]) + else: + items.append((typehint, val)) + elif isinstance(values,tuple): + items = [values[:2]] + else: + items = [(typehint, values)] + + return items + + def __setitem__(self, i, val): + """Set indicatated argument (or slice) to a new value. + 'val' can be a single int/float/string, or a (typehint, value) tuple. + Or, if 'i' is a slice, a list of these or another OSCMessage. + """ + items = list(self.items()) + + new_items = self._buildItemList(val) + + if not isinstance(i,slice): + if len(new_items) != 1: + raise TypeError("single-item assignment expects a single value or a (typetag, value) tuple") + + new_items = new_items[0] + + # finally... + items[i] = new_items + + self._reencode(items) + + def setItem(self, i, val, typehint=None): + """Set indicated argument to a new value (with typehint) + """ + items = list(self.items()) + + items[i] = (typehint, val) + + self._reencode(items) + + def copy(self): + """Returns a deep copy of this OSCMessage + """ + msg = self.__class__(self.address) + msg.typetags = self.typetags + msg.message = self.message + return msg + + def count(self, val): + """Returns the number of times the given value occurs in the OSCMessage's arguments + """ + return list(self.values()).count(val) + + def index(self, val): + """Returns the index of the first occurence of the given value in the OSCMessage's arguments. + Raises ValueError if val isn't found + """ + return list(self.values()).index(val) + + def extend(self, values): + """Append the contents of 'values' to this OSCMessage. + 'values' can be another OSCMessage, or a list/tuple of ints/floats/strings + """ + items = list(self.items()) + self._buildItemList(values) + + self._reencode(items) + + def insert(self, i, val, typehint = None): + """Insert given value (with optional typehint) into the OSCMessage + at the given index. + """ + items = list(self.items()) + + for item in reversed(self._buildItemList(val)): + items.insert(i, item) + + self._reencode(items) + + def popitem(self, i): + """Delete the indicated argument from the OSCMessage, and return it + as a (typetag, value) tuple. + """ + items = list(self.items()) + + item = items.pop(i) + + self._reencode(items) + + return item + + def pop(self, i): + """Delete the indicated argument from the OSCMessage, and return it. + """ + return self.popitem(i)[1] + + def reverse(self): + """Reverses the arguments of the OSCMessage (in place) + """ + items = list(self.items()) + + items.reverse() + + self._reencode(items) + + def remove(self, val): + """Removes the first argument with the given value from the OSCMessage. + Raises ValueError if val isn't found. + """ + items = list(self.items()) + + # this is not very efficient... + i = 0 + for (t, v) in items: + if (v == val): + break + i += 1 + else: + raise ValueError("'%s' not in OSCMessage" % str(m)) + # but more efficient than first calling self.values().index(val), + # then calling self.items(), which would in turn call self.values() again... + + del items[i] + + self._reencode(items) + + def __iter__(self): + """Returns an iterator of the OSCMessage's arguments + """ + return iter(list(self.values())) + + def __reversed__(self): + """Returns a reverse iterator of the OSCMessage's arguments + """ + return reversed(list(self.values())) + + def itervalues(self): + """Returns an iterator of the OSCMessage's arguments + """ + return iter(list(self.values())) + + def iteritems(self): + """Returns an iterator of the OSCMessage's arguments as + (typetag, value) tuples + """ + return iter(list(self.items())) + + def itertags(self): + """Returns an iterator of the OSCMessage's arguments' typetags + """ + return iter(self.tags()) + +class OSCBundle(OSCMessage): + """Builds a 'bundle' of OSC messages. + + OSCBundle objects are container objects for building OSC-bundles of OSC-messages. + An OSC-bundle is a special kind of OSC-message which contains a list of OSC-messages + (And yes, OSC-bundles may contain other OSC-bundles...) + + OSCBundle objects behave much the same as OSCMessage objects, with these exceptions: + - if an item or items to be appended or inserted are not OSCMessage objects, + OSCMessage objectss are created to encapsulate the item(s) + - an OSC-bundle does not have an address of its own, only the contained OSC-messages do. + The OSCBundle's 'address' is inherited by any OSCMessage the OSCBundle object creates. + - OSC-bundles have a timetag to tell the receiver when the bundle should be processed. + The default timetag value (0) means 'immediately' + """ + def __init__(self, address="", time=0): + """Instantiate a new OSCBundle. + The default OSC-address for newly created OSCMessages + can be specified with the 'address' argument + The bundle's timetag can be set with the 'time' argument + """ + super(OSCBundle, self).__init__(address) + self.timetag = time + + def __str__(self): + """Returns the Bundle's contents (and timetag, if nonzero) as a string. + """ + if (self.timetag > 0.): + out = "#bundle (%s) [" % self.getTimeTagStr() + else: + out = "#bundle [" + + if self.__len__(): + for val in list(self.values()): + out += "%s, " % str(val) + out = out[:-2] # strip trailing space and comma + + return out + "]" + + def setTimeTag(self, time): + """Set or change the OSCBundle's TimeTag + In 'Python Time', that's floating seconds since the Epoch + """ + if time >= 0: + self.timetag = time + + def getTimeTagStr(self): + """Return the TimeTag as a human-readable string + """ + fract, secs = math.modf(self.timetag) + out = time.ctime(secs)[11:19] + out += ("%.3f" % fract)[1:] + + return out + + def append(self, argument, typehint = None): + """Appends data to the bundle, creating an OSCMessage to encapsulate + the provided argument unless this is already an OSCMessage. + Any newly created OSCMessage inherits the OSCBundle's address at the time of creation. + If 'argument' is an iterable, its elements will be encapsuated by a single OSCMessage. + Finally, 'argument' can be (or contain) a dict, which will be 'converted' to an OSCMessage; + - if 'addr' appears in the dict, its value overrides the OSCBundle's address + - if 'args' appears in the dict, its value(s) become the OSCMessage's arguments + """ + if isinstance(argument, OSCMessage): + binary = OSCBlob(argument.getBinary()) + else: + msg = OSCMessage(self.address) + if isinstance(argument,dict): + if 'addr' in argument: + msg.setAddress(argument['addr']) + if 'args' in argument: + msg.append(argument['args'], typehint) + else: + msg.append(argument, typehint) + + binary = OSCBlob(msg.getBinary()) + + self.message += binary + self.typetags += 'b' + + def getBinary(self): + """Returns the binary representation of the message + """ + binary = OSCString("#bundle") + binary += OSCTimeTag(self.timetag) + binary += self.message + + return binary + + def _reencapsulate(self, decoded): + if decoded[0] == "#bundle": + msg = OSCBundle() + msg.setTimeTag(decoded[1]) + for submsg in decoded[2:]: + msg.append(self._reencapsulate(submsg)) + + else: + msg = OSCMessage(decoded[0]) + tags = decoded[1].lstrip(',') + for i in range(len(tags)): + msg.append(decoded[2+i], tags[i]) + + return msg + + def values(self): + """Returns a list of the OSCMessages appended so far + """ + out = [] + for decoded in decodeOSC(self.getBinary())[2:]: + out.append(self._reencapsulate(decoded)) + + return out + + def __eq__(self, other): + """Return True if two OSCBundles have the same timetag & content + """ + if not isinstance(other, self.__class__): + return False + + return (self.timetag == other.timetag) and (self.typetags == other.typetags) and (self.message == other.message) + + def copy(self): + """Returns a deep copy of this OSCBundle + """ + copy = super(OSCBundle, self).copy() + copy.timetag = self.timetag + return copy + +###### +# +# OSCMessage encoding functions +# +###### + +def OSCString(next): + """Convert a string into a zero-padded OSC String. + The length of the resulting string is always a multiple of 4 bytes. + The string ends with 1 to 4 zero-bytes ('\x00') + """ + + OSCstringLength = math.ceil((len(next)+1) / 4.0) * 4 + return struct.pack(">%ds" % (OSCstringLength), str(next).encode('latin1')) + +def OSCBlob(next): + """Convert a string into an OSC Blob. + An OSC-Blob is a binary encoded block of data, prepended by a 'size' (int32). + The size is always a mutiple of 4 bytes. + The blob ends with 0 to 3 zero-bytes ('\x00') + """ + + if isinstance(next,str): + next = next.encode('latin1') + if isinstance(next,bytes): + OSCblobLength = math.ceil((len(next)) / 4.0) * 4 + binary = struct.pack(">i%ds" % (OSCblobLength), OSCblobLength, next) + else: + binary = b'' + + return binary + +def OSCArgument(next, typehint=None): + """ Convert some Python types to their + OSC binary representations, returning a + (typetag, data) tuple. + """ + if not typehint: + if type(next) in FloatTypes: + binary = struct.pack(">f", float(next)) + tag = 'f' + elif type(next) in IntTypes: + binary = struct.pack(">i", int(next)) + tag = 'i' + else: + binary = OSCString(next) + tag = 's' + + elif typehint == 'd': + try: + binary = struct.pack(">d", float(next)) + tag = 'd' + except ValueError: + binary = OSCString(next) + tag = 's' + + elif typehint == 'f': + try: + binary = struct.pack(">f", float(next)) + tag = 'f' + except ValueError: + binary = OSCString(next) + tag = 's' + elif typehint == 'i': + try: + binary = struct.pack(">i", int(next)) + tag = 'i' + except ValueError: + binary = OSCString(next) + tag = 's' + else: + binary = OSCString(next) + tag = 's' + + return (tag, binary) + +def OSCTimeTag(time): + """Convert a time in floating seconds to its + OSC binary representation + """ + if time > 0: + fract, secs = math.modf(time) + secs = secs - NTP_epoch + binary = struct.pack('>LL', int(secs), int(fract * NTP_units_per_second)) + else: + binary = struct.pack('>LL', 0, 1) + + return binary + +###### +# +# OSCMessage decoding functions +# +###### + +def _readString(data): + """Reads the next (null-terminated) block of data + """ + length = data.find(b'\0') + nextData = int(math.ceil((length+1) / 4.0) * 4) + return (data[0:length].decode('latin1'), data[nextData:]) + +def _readBlob(data): + """Reads the next (numbered) block of data + """ + + length = struct.unpack(">i", data[0:4])[0] + nextData = int(math.ceil((length) / 4.0) * 4) + 4 + return (data[4:length+4], data[nextData:]) + +def _readInt(data): + """Tries to interpret the next 4 bytes of the data + as a 32-bit integer. """ + + if(len(data)<4): + print("Error: too few bytes for int", data, len(data)) + rest = data + integer = 0 + else: + integer = struct.unpack(">i", data[0:4])[0] + rest = data[4:] + + return (integer, rest) + +def _readLong(data): + """Tries to interpret the next 8 bytes of the data + as a 64-bit signed integer. + """ + + high, low = struct.unpack(">ll", data[0:8]) + big = (int(high) << 32) + low + rest = data[8:] + return (big, rest) + +def _readTimeTag(data): + """Tries to interpret the next 8 bytes of the data + as a TimeTag. + """ + high, low = struct.unpack(">LL", data[0:8]) + if (high == 0) and (low <= 1): + time = 0.0 + else: + time = int(NTP_epoch + high) + float(low / NTP_units_per_second) + rest = data[8:] + return (time, rest) + +def _readFloat(data): + """Tries to interpret the next 4 bytes of the data + as a 32-bit float. + """ + + if(len(data)<4): + print("Error: too few bytes for float", data, len(data)) + rest = data + float = 0 + else: + float = struct.unpack(">f", data[0:4])[0] + rest = data[4:] + + return (float, rest) + +def _readDouble(data): + """Tries to interpret the next 8 bytes of the data + as a 64-bit float. + """ + + if(len(data)<8): + print("Error: too few bytes for double", data, len(data)) + rest = data + float = 0 + else: + float = struct.unpack(">d", data[0:8])[0] + rest = data[8:] + + return (float, rest) + +def decodeOSC(data): + """Converts a binary OSC message to a Python list. + """ + table = {"i":_readInt, "f":_readFloat, "s":_readString, "b":_readBlob, "d":_readDouble, "t":_readTimeTag} + decoded = [] + address, rest = _readString(data) + if address.startswith(","): + typetags = address + address = "" + else: + typetags = "" + + if address == "#bundle": + time, rest = _readTimeTag(rest) + decoded.append(address) + decoded.append(time) + while len(rest)>0: + length, rest = _readInt(rest) + decoded.append(decodeOSC(rest[:length])) + rest = rest[length:] + + elif len(rest)>0: + if not len(typetags): + typetags, rest = _readString(rest) + decoded.append(address) + decoded.append(typetags) + if typetags.startswith(","): + for tag in typetags[1:]: + value, rest = table[tag](rest) + decoded.append(value) + else: + raise OSCError("OSCMessage's typetag-string lacks the magic ','") + + return decoded + +###### +# +# Utility functions +# +###### + +def hexDump(bytes): + """ Useful utility; prints the string in hexadecimal. + """ + print("byte 0 1 2 3 4 5 6 7 8 9 A B C D E F") + + if isinstance(bytes,str): + bytes = bytes.encode('latin1') + num = len(bytes) + for i in range(num): + if (i) % 16 == 0: + line = "%02X0 : " % (i/16) + line += "%02X " % bytes[i] + if (i+1) % 16 == 0: + print("%s: %s" % (line, repr(bytes[i-15:i+1]))) + line = "" + + bytes_left = num % 16 + if bytes_left: + print("%s: %s" % (line.ljust(54), repr(bytes[-bytes_left:]))) + +def getUrlStr(*args): + """Convert provided arguments to a string in 'host:port/prefix' format + Args can be: + - (host, port) + - (host, port), prefix + - host, port + - host, port, prefix + """ + if not len(args): + return "" + + if type(args[0]) == tuple: + host = args[0][0] + port = args[0][1] + args = args[1:] + else: + host = args[0] + port = args[1] + args = args[2:] + + if len(args): + prefix = args[0] + else: + prefix = "" + + if len(host) and (host != '0.0.0.0'): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + else: + host = 'localhost' + + if isinstance(port,int): + return "%s:%d%s" % (host, port, prefix) + else: + return host + prefix + +def parseUrlStr(url): + """Convert provided string in 'host:port/prefix' format to it's components + Returns ((host, port), prefix) + """ + if not (isinstance(url,str) and len(url)): + return (None, '') + + i = url.find("://") + if i > -1: + url = url[i+3:] + + i = url.find(':') + if i > -1: + host = url[:i].strip() + tail = url[i+1:].strip() + else: + host = '' + tail = url + + for i in range(len(tail)): + if not tail[i].isdigit(): + break + else: + i += 1 + + portstr = tail[:i].strip() + tail = tail[i:].strip() + + found = len(tail) + for c in ('/', '+', '-', '*'): + i = tail.find(c) + if (i > -1) and (i < found): + found = i + + head = tail[:found].strip() + prefix = tail[found:].strip() + + prefix = prefix.strip('/') + if len(prefix) and prefix[0] not in ('+', '-', '*'): + prefix = '/' + prefix + + if len(head) and not len(host): + host = head + + if len(host): + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + try: + port = int(portstr) + except ValueError: + port = None + + return ((host, port), prefix) + +###### +# +# OSCClient class +# +###### + +class OSCClient(object): + """Simple OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket + """ + # set outgoing socket buffer size + sndbuf_size = 4096 * 8 + + def __init__(self, server=None): + """Construct an OSC Client. + When the 'address' argument is given this client is connected to a specific remote server. + - address ((host, port) tuple): the address of the remote server to send all messages to + Otherwise it acts as a generic client: + If address == 'None', the client doesn't connect to a specific remote server, + and the remote address must be supplied when calling sendto() + - server: Local OSCServer-instance this client will use the socket of for transmissions. + If none is supplied, a socket will be created. + """ + self.socket = None + + if server == None: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self._fd = self.socket.fileno() + + self.server = None + else: + self.setServer(server) + + self.client_address = None + + def setServer(self, server): + """Associate this Client with given server. + The Client will send from the Server's socket. + The Server will use this Client instance to send replies. + """ + if not isinstance(server, OSCServer): + raise ValueError("'server' argument is not a valid OSCServer object") + + if self.socket != None: + self.close() + + self.socket = server.socket.dup() + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self._fd = self.socket.fileno() + + self.server = server + + if self.server.client != None: + self.server.client.close() + + self.server.client = self + + def close(self): + """Disconnect & close the Client's socket + """ + if self.socket != None: + self.socket.close() + self.socket = None + + def __str__(self): + """Returns a string containing this Client's Class-name, software-version + and the remote-address it is connected to (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.address() + if addr: + out += " connected to osc://%s" % getUrlStr(addr) + else: + out += " (unconnected)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + isequal = cmp(self.socket._sock, other.socket._sock) + if isequal and self.server and other.server: + return cmp(self.server, other.server) + + return isequal + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) + + def address(self): + """Returns a (host,port) tuple of the remote server this client is + connected to or None if not connected to any server. + """ + try: + return self.socket.getpeername() + except socket.error: + return None + + def connect(self, address): + """Bind to a specific OSC server: + the 'address' argument is a (host, port) tuple + - host: hostname of the remote OSC server, + - port: UDP-port the remote OSC server listens to. + """ + try: + self.socket.connect(address) + self.client_address = address + except socket.error as e: + self.client_address = None + raise OSCClientError("SocketError: %s" % str(e)) + + if self.server != None: + self.server.return_port = address[1] + + def sendto(self, msg, address, timeout=None): + """Send the given OSCMessage to the specified address. + - msg: OSCMessage (or OSCBundle) to be sent + - address: (host, port) tuple specifing remote server to send the message to + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + self.socket.connect(address) + self.socket.sendall(msg.getBinary()) + + if self.client_address: + self.socket.connect(self.client_address) + + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending to %s: %s" % (str(address), str(e))) + + def send(self, msg, timeout=None): + """Send the given OSCMessage. + The Client must be already connected. + - msg: OSCMessage (or OSCBundle) to be sent + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket, + or when the Client isn't connected to a remote server. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + self.socket.sendall(msg.getBinary()) + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending: %s" % str(e)) + +###### +# +# FilterString Utility functions +# +###### + +def parseFilterStr(args): + """Convert Message-Filter settings in '+ - ...' format to a dict of the form + { '':True, '':False, ... } + Returns a list: ['', filters] + """ + out = {} + + if isinstance(args,str): + args = [args] + + prefix = None + for arg in args: + head = None + for plus in arg.split('+'): + minus = plus.split('-') + plusfs = minus.pop(0).strip() + if len(plusfs): + plusfs = '/' + plusfs.strip('/') + + if (head == None) and (plusfs != "/*"): + head = plusfs + elif len(plusfs): + if plusfs == '/*': + out = { '/*':True } # reset all previous filters + else: + out[plusfs] = True + + for minusfs in minus: + minusfs = minusfs.strip() + if len(minusfs): + minusfs = '/' + minusfs.strip('/') + if minusfs == '/*': + out = { '/*':False } # reset all previous filters + else: + out[minusfs] = False + + if prefix == None: + prefix = head + + return [prefix, out] + +def getFilterStr(filters): + """Return the given 'filters' dict as a list of + '+' | '-' filter-strings + """ + if not len(filters): + return [] + + if '/*' in list(filters.keys()): + if filters['/*']: + out = ["+/*"] + else: + out = ["-/*"] + else: + if False in list(filters.values()): + out = ["+/*"] + else: + out = ["-/*"] + + for (addr, bool) in list(filters.items()): + if addr == '/*': + continue + + if bool: + out.append("+%s" % addr) + else: + out.append("-%s" % addr) + + return out + +# A translation-table for mapping OSC-address expressions to Python 're' expressions +OSCtrans = str.maketrans("{,}?","(|).") + +def getRegEx(pattern): + """Compiles and returns a 'regular expression' object for the given address-pattern. + """ + # Translate OSC-address syntax to python 're' syntax + pattern = pattern.replace(".", r"\.") # first, escape all '.'s in the pattern. + pattern = pattern.replace("(", r"\(") # escape all '('s. + pattern = pattern.replace(")", r"\)") # escape all ')'s. + pattern = pattern.replace("*", r".*") # replace a '*' by '.*' (match 0 or more characters) + pattern = pattern.translate(OSCtrans) # change '?' to '.' and '{,}' to '(|)' + + return re.compile(pattern) + +###### +# +# OSCMultiClient class +# +###### + +class OSCMultiClient(OSCClient): + """'Multiple-Unicast' OSC Client. Handles the sending of OSC-Packets (OSCMessage or OSCBundle) via a UDP-socket + This client keeps a dict of 'OSCTargets'. and sends each OSCMessage to each OSCTarget + The OSCTargets are simply (host, port) tuples, and may be associated with an OSC-address prefix. + the OSCTarget's prefix gets prepended to each OSCMessage sent to that target. + """ + def __init__(self, server=None): + """Construct a "Multi" OSC Client. + - server: Local OSCServer-instance this client will use the socket of for transmissions. + If none is supplied, a socket will be created. + """ + super(OSCMultiClient, self).__init__(server) + + self.targets = {} + + def _searchHostAddr(self, host): + """Search the subscribed OSCTargets for (the first occurence of) given host. + Returns a (host, port) tuple + """ + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + for addr in list(self.targets.keys()): + if host == addr[0]: + return addr + + raise NotSubscribedError((host, None)) + + def _updateFilters(self, dst, src): + """Update a 'filters' dict with values form another 'filters' dict: + - src[a] == True and dst[a] == False: del dst[a] + - src[a] == False and dst[a] == True: del dst[a] + - a not in dst: dst[a] == src[a] + """ + if '/*' in list(src.keys()): # reset filters + dst.clear() # 'match everything' == no filters + if not src.pop('/*'): + dst['/*'] = False # 'match nothing' + + for (addr, bool) in list(src.items()): + if (addr in list(dst.keys())) and (dst[addr] != bool): + del dst[addr] + else: + dst[addr] = bool + + def _setTarget(self, address, prefix=None, filters=None): + """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget. + - address ((host, port) tuple): IP-address & UDP-port + - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage + sent to this OSCTarget (optional) + """ + if address not in list(self.targets.keys()): + self.targets[address] = ["",{}] + + if prefix != None: + if len(prefix): + # make sure prefix starts with ONE '/', and does not end with '/' + prefix = '/' + prefix.strip('/') + + self.targets[address][0] = prefix + + if filters != None: + if isinstance(filters,str): + (_, filters) = parseFilterStr(filters) + elif not isinstance(filters,dict): + raise TypeError("'filters' argument must be a dict with {addr:bool} entries") + + self._updateFilters(self.targets[address][1], filters) + + def setOSCTarget(self, address, prefix=None, filters=None): + """Add (i.e. subscribe) a new OSCTarget, or change the prefix for an existing OSCTarget. + the 'address' argument can be a ((host, port) tuple) : The target server address & UDP-port + or a 'host' (string) : The host will be looked-up + - prefix (string): The OSC-address prefix prepended to the address of each OSCMessage + sent to this OSCTarget (optional) + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + elif (isinstance(address,tuple)): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except: + pass + + address = (host, port) + else: + raise TypeError("'address' argument must be a (host, port) tuple or a 'host' string") + + self._setTarget(address, prefix, filters) + + def setOSCTargetFromStr(self, url): + """Adds or modifies a subscribed OSCTarget from the given string, which should be in the + ':[/] [+/]|[-/] ...' format. + """ + (addr, tail) = parseUrlStr(url) + (prefix, filters) = parseFilterStr(tail) + self._setTarget(addr, prefix, filters) + + def _delTarget(self, address, prefix=None): + """Delete the specified OSCTarget from the Client's dict. + the 'address' argument must be a (host, port) tuple. + If the 'prefix' argument is given, the Target is only deleted if the address and prefix match. + """ + try: + if prefix == None: + del self.targets[address] + elif prefix == self.targets[address][0]: + del self.targets[address] + except KeyError: + raise NotSubscribedError(address, prefix) + + def delOSCTarget(self, address, prefix=None): + """Delete the specified OSCTarget from the Client's dict. + the 'address' argument can be a ((host, port) tuple), or a hostname. + If the 'prefix' argument is given, the Target is only deleted if the address and prefix match. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if isinstance(address,tuple): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + self._delTarget(address, prefix) + + def hasOSCTarget(self, address, prefix=None): + """Return True if the given OSCTarget exists in the Client's dict. + the 'address' argument can be a ((host, port) tuple), or a hostname. + If the 'prefix' argument is given, the return-value is only True if the address and prefix match. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if isinstance(address,tuple): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + if address in list(self.targets.keys()): + if prefix == None: + return True + elif prefix == self.targets[address][0]: + return True + + return False + + def getOSCTargets(self): + """Returns the dict of OSCTargets: {addr:[prefix, filters], ...} + """ + out = {} + for ((host, port), pf) in list(self.targets.items()): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + + out[(host, port)] = pf + + return out + + def getOSCTarget(self, address): + """Returns the OSCTarget matching the given address as a ((host, port), [prefix, filters]) tuple. + 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned + Returns (None, ['',{}]) if address not found. + """ + if isinstance(address,str): + address = self._searchHostAddr(address) + + if (isinstance(address,tuple)): + (host, port) = address[:2] + try: + host = socket.gethostbyname(host) + except socket.error: + pass + address = (host, port) + + if (address in list(self.targets.keys())): + try: + (host, _, _) = socket.gethostbyaddr(host) + except socket.error: + pass + + return ((host, port), self.targets[address]) + + return (None, ['',{}]) + + def clearOSCTargets(self): + """Erases all OSCTargets from the Client's dict + """ + self.targets = {} + + def updateOSCTargets(self, dict): + """Update the Client's OSCTargets dict with the contents of 'dict' + The given dict's items MUST be of the form + { (host, port):[prefix, filters], ... } + """ + for ((host, port), (prefix, filters)) in list(dict.items()): + val = [prefix, {}] + self._updateFilters(val[1], filters) + + try: + host = socket.gethostbyname(host) + except socket.error: + pass + + self.targets[(host, port)] = val + + def getOSCTargetStr(self, address): + """Returns the OSCTarget matching the given address as a ('osc://:[]', ['', ...])' tuple. + 'address' can be a (host, port) tuple, or a 'host' (string), in which case the first matching OSCTarget is returned + Returns (None, []) if address not found. + """ + (addr, (prefix, filters)) = self.getOSCTarget(address) + if addr == None: + return (None, []) + + return ("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters)) + + def getOSCTargetStrings(self): + """Returns a list of all OSCTargets as ('osc://:[]', ['', ...])' tuples. + """ + out = [] + for (addr, (prefix, filters)) in list(self.targets.items()): + out.append(("osc://%s" % getUrlStr(addr, prefix), getFilterStr(filters))) + + return out + + def connect(self, address): + """The OSCMultiClient isn't allowed to connect to any specific + address. + """ + return NotImplemented + + def sendto(self, msg, address, timeout=None): + """Send the given OSCMessage. + The specified address is ignored. Instead this method calls send() to + send the message to all subscribed clients. + - msg: OSCMessage (or OSCBundle) to be sent + - address: (host, port) tuple specifing remote server to send the message to + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + self.send(msg, timeout) + + def _filterMessage(self, filters, msg): + """Checks the given OSCMessge against the given filters. + 'filters' is a dict containing OSC-address:bool pairs. + If 'msg' is an OSCBundle, recursively filters its constituents. + Returns None if the message is to be filtered, else returns the message. + or + Returns a copy of the OSCBundle with the filtered messages removed. + """ + if isinstance(msg, OSCBundle): + out = msg.copy() + msgs = list(out.values()) + out.clearData() + for m in msgs: + m = self._filterMessage(filters, m) + if m: # this catches 'None' and empty bundles. + out.append(m) + + elif isinstance(msg, OSCMessage): + if '/*' in list(filters.keys()): + if filters['/*']: + out = msg + else: + out = None + + elif False in list(filters.values()): + out = msg + else: + out = None + + else: + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + expr = getRegEx(msg.address) + + for addr in list(filters.keys()): + if addr == '/*': + continue + + match = expr.match(addr) + if match and (match.end() == len(addr)): + if filters[addr]: + out = msg + else: + out = None + break + + return out + + def _prefixAddress(self, prefix, msg): + """Makes a copy of the given OSCMessage, then prepends the given prefix to + The message's OSC-address. + If 'msg' is an OSCBundle, recursively prepends the prefix to its constituents. + """ + out = msg.copy() + + if isinstance(msg, OSCBundle): + msgs = list(out.values()) + out.clearData() + for m in msgs: + out.append(self._prefixAddress(prefix, m)) + + elif isinstance(msg, OSCMessage): + out.setAddress(prefix + out.address) + + else: + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + return out + + def send(self, msg, timeout=None): + """Send the given OSCMessage to all subscribed OSCTargets + - msg: OSCMessage (or OSCBundle) to be sent + - timeout: A timeout value for attempting to send. If timeout == None, + this call blocks until socket is available for writing. + Raises OSCClientError when timing out while waiting for the socket. + """ + for (address, (prefix, filters)) in list(self.targets.items()): + if len(filters): + out = self._filterMessage(filters, msg) + if not out: # this catches 'None' and empty bundles. + continue + else: + out = msg + + if len(prefix): + out = self._prefixAddress(prefix, msg) + + binary = out.getBinary() + + ret = select.select([],[self._fd], [], timeout) + try: + ret[1].index(self._fd) + except: + # for the very rare case this might happen + raise OSCClientError("Timed out waiting for file descriptor") + + try: + while len(binary): + sent = self.socket.sendto(binary, address) + binary = binary[sent:] + + except socket.error as e: + if e[0] in (7, 65): # 7 = 'no address associated with nodename', 65 = 'no route to host' + raise e + else: + raise OSCClientError("while sending to %s: %s" % (str(address), str(e))) + +class OSCAddressSpace: + def __init__(self): + self.callbacks = {} + def addMsgHandler(self, address, callback): + """Register a handler for an OSC-address + - 'address' is the OSC address-string. + the address-string should start with '/' and may not contain '*' + - 'callback' is the function called for incoming OSCMessages that match 'address'. + The callback-function will be called with the same arguments as the 'msgPrinter_handler' below + """ + for chk in '*?,[]{}# ': + if chk in address: + raise OSCServerError("OSC-address string may not contain any characters in '*?,[]{}# '") + + if type(callback) not in (types.FunctionType, types.MethodType): + raise OSCServerError("Message callback '%s' is not callable" % repr(callback)) + + if address != 'default': + address = '/' + address.strip('/') + + self.callbacks[address] = callback + + def delMsgHandler(self, address): + """Remove the registered handler for the given OSC-address + """ + del self.callbacks[address] + + def getOSCAddressSpace(self): + """Returns a list containing all OSC-addresses registerd with this Server. + """ + return list(self.callbacks.keys()) + + def dispatchMessage(self, pattern, tags, data, client_address): + """Attmept to match the given OSC-address pattern, which may contain '*', + against all callbacks registered with the OSCServer. + Calls the matching callback and returns whatever it returns. + If no match is found, and a 'default' callback is registered, it calls that one, + or raises NoCallbackError if a 'default' callback is not registered. + + - pattern (string): The OSC-address of the receied message + - tags (string): The OSC-typetags of the receied message's arguments, without ',' + - data (list): The message arguments + """ + if len(tags) != len(data): + raise OSCServerError("Malformed OSC-message; got %d typetags [%s] vs. %d values" % (len(tags), tags, len(data))) + + expr = getRegEx(pattern) + + replies = [] + matched = 0 + for addr in list(self.callbacks.keys()): + match = expr.match(addr) + if match and (match.end() == len(addr)): + reply = self.callbacks[addr](pattern, tags, data, client_address) + matched += 1 + if isinstance(reply, OSCMessage): + replies.append(reply) + elif reply != None: + raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks[addr], type(reply))) + + if matched == 0: + if 'default' in self.callbacks: + reply = self.callbacks['default'](pattern, tags, data, client_address) + if isinstance(reply, OSCMessage): + replies.append(reply) + elif reply != None: + raise TypeError("Message-callback %s did not return OSCMessage or None: %s" % (self.server.callbacks['default'], type(reply))) + else: + raise NoCallbackError(pattern) + + return replies + +###### +# +# OSCRequestHandler classes +# +###### +class OSCRequestHandler(DatagramRequestHandler): + """RequestHandler class for the OSCServer + """ + def setup(self): + """Prepare RequestHandler. + Unpacks request as (packet, source socket address) + Creates an empty list for replies. + """ + (self.packet, self.socket) = self.request + self.replies = [] + + def _unbundle(self, decoded): + """Recursive bundle-unpacking function""" + if decoded[0] != "#bundle": + self.replies += self.server.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def handle(self): + """Handle incoming OSCMessage + """ + decoded = decodeOSC(self.packet) + if not len(decoded): + return + + self._unbundle(decoded) + + def finish(self): + """Finish handling OSCMessage. + Send any reply returned by the callback(s) back to the originating client + as an OSCMessage or OSCBundle + """ + if self.server.return_port: + self.client_address = (self.client_address[0], self.server.return_port) + + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + return + + self.server.client.sendto(msg, self.client_address) + +class ThreadingOSCRequestHandler(OSCRequestHandler): + """Multi-threaded OSCRequestHandler; + Starts a new RequestHandler thread for each unbundled OSCMessage + """ + def _unbundle(self, decoded): + """Recursive bundle-unpacking function + This version starts a new thread for each sub-Bundle found in the Bundle, + then waits for all its children to finish. + """ + if decoded[0] != "#bundle": + self.replies += self.server.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + now = time.time() + + children = [] + + for msg in decoded[2:]: + t = threading.Thread(target = self._unbundle, args = (msg,)) + t.start() + children.append(t) + + # wait for all children to terminate + for t in children: + t.join() + +###### +# +# OSCServer classes +# +###### +class OSCServer(UDPServer, OSCAddressSpace): + """A Synchronous OSCServer + Serves one request at-a-time, until the OSCServer is closed. + The OSC address-pattern is matched against a set of OSC-adresses + that have been registered to the server with a callback-function. + If the adress-pattern of the message machtes the registered address of a callback, + that function is called. + """ + + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = OSCRequestHandler + + # define a socket timeout, so the serve_forever loop can actually exit. + socket_timeout = 1 + + # DEBUG: print error-tracebacks (to stderr)? + print_tracebacks = False + + def __init__(self, server_address, client=None, return_port=0): + """Instantiate an OSCServer. + - server_address ((host, port) tuple): the local host & UDP-port + the server listens on + - client (OSCClient instance): The OSCClient used to send replies from this server. + If none is supplied (default) an OSCClient will be created. + - return_port (int): if supplied, sets the default UDP destination-port + for replies coming from this server. + """ + UDPServer.__init__(self, server_address, self.RequestHandlerClass) + OSCAddressSpace.__init__(self) + + self.setReturnPort(return_port) + self.error_prefix = "" + self.info_prefix = "/info" + + self.socket.settimeout(self.socket_timeout) + + self.running = False + self.client = None + + if client == None: + self.client = OSCClient(server=self) + else: + self.setClient(client) + + def setClient(self, client): + """Associate this Server with a new local Client instance, closing the Client this Server is currently using. + """ + if not isinstance(client, OSCClient): + raise ValueError("'client' argument is not a valid OSCClient object") + + if client.server != None: + raise OSCServerError("Provided OSCClient already has an OSCServer-instance: %s" % str(client.server)) + + # Server socket is already listening at this point, so we can't use the client's socket. + # we'll have to force our socket on the client... + client_address = client.address() # client may be already connected + client.close() # shut-down that socket + + # force our socket upon the client + client.socket = self.socket.dup() + client.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, client.sndbuf_size) + client._fd = client.socket.fileno() + client.server = self + + if client_address: + client.connect(client_address) + if not self.return_port: + self.return_port = client_address[1] + + if self.client != None: + self.client.close() + + self.client = client + + def serve_forever(self): + """Handle one request at a time until server is closed.""" + self.running = True + while self.running: + self.handle_request() # this times-out when no data arrives. + + def close(self): + """Stops serving requests, closes server (socket), closes used client + """ + self.running = False + self.client.close() + self.server_close() + + def __str__(self): + """Returns a string containing this Server's Class-name, software-version and local bound address (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.address() + if addr: + out += " listening on osc://%s" % getUrlStr(addr) + else: + out += " (unbound)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + return cmp(self.socket._sock, other.socket._sock) + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) + + def address(self): + """Returns a (host,port) tuple of the local address this server is bound to, + or None if not bound to any address. + """ + try: + return self.socket.getsockname() + except socket.error: + return None + + def setReturnPort(self, port): + """Set the destination UDP-port for replies returning from this server to the remote client + """ + if (port > 1024) and (port < 65536): + self.return_port = port + else: + self.return_port = None + + + def setSrvInfoPrefix(self, pattern): + """Set the first part of OSC-address (pattern) this server will use to reply to server-info requests. + """ + if len(pattern): + pattern = '/' + pattern.strip('/') + + self.info_prefix = pattern + + def setSrvErrorPrefix(self, pattern=""): + """Set the OSC-address (pattern) this server will use to report errors occuring during + received message handling to the remote client. + + If pattern is empty (default), server-errors are not reported back to the client. + """ + if len(pattern): + pattern = '/' + pattern.strip('/') + + self.error_prefix = pattern + + def addDefaultHandlers(self, prefix="", info_prefix="/info", error_prefix="/error"): + """Register a default set of OSC-address handlers with this Server: + - 'default' -> noCallback_handler + the given prefix is prepended to all other callbacks registered by this method: + - ' serverInfo_handler + - ' -> msgPrinter_handler + - '/print' -> msgPrinter_handler + and, if the used Client supports it; + - '/subscribe' -> subscription_handler + - '/unsubscribe' -> subscription_handler + + Note: the given 'error_prefix' argument is also set as default 'error_prefix' for error-messages + *sent from* this server. This is ok, because error-messages generally do not elicit a reply from the receiver. + + To do this with the serverInfo-prefixes would be a bad idea, because if a request received on '/info' (for example) + would send replies to '/info', this could potentially cause a never-ending loop of messages! + Do *not* set the 'info_prefix' here (for incoming serverinfo requests) to the same value as given to + the setSrvInfoPrefix() method (for *replies* to incoming serverinfo requests). + For example, use '/info' for incoming requests, and '/inforeply' or '/serverinfo' or even just '/print' as the + info-reply prefix. + """ + self.error_prefix = error_prefix + self.addMsgHandler('default', self.noCallback_handler) + self.addMsgHandler(prefix + info_prefix, self.serverInfo_handler) + self.addMsgHandler(prefix + error_prefix, self.msgPrinter_handler) + self.addMsgHandler(prefix + '/print', self.msgPrinter_handler) + + if isinstance(self.client, OSCMultiClient): + self.addMsgHandler(prefix + '/subscribe', self.subscription_handler) + self.addMsgHandler(prefix + '/unsubscribe', self.subscription_handler) + + def printErr(self, txt): + """Writes 'OSCServer: txt' to sys.stderr + """ + sys.stderr.write("OSCServer: %s\n" % txt) + + def sendOSCerror(self, txt, client_address): + """Sends 'txt', encapsulated in an OSCMessage to the default 'error_prefix' OSC-addres. + Message is sent to the given client_address, with the default 'return_port' overriding + the client_address' port, if defined. + """ + lines = txt.split('\n') + if len(lines) == 1: + msg = OSCMessage(self.error_prefix) + msg.append(lines[0]) + elif len(lines) > 1: + msg = OSCBundle(self.error_prefix) + for line in lines: + msg.append(line) + else: + return + + if self.return_port: + client_address = (client_address[0], self.return_port) + + self.client.sendto(msg, client_address) + + def reportErr(self, txt, client_address): + """Writes 'OSCServer: txt' to sys.stderr + If self.error_prefix is defined, sends 'txt' as an OSC error-message to the client(s) + (see printErr() and sendOSCerror()) + """ + self.printErr(txt) + + if len(self.error_prefix): + self.sendOSCerror(txt, client_address) + + def sendOSCinfo(self, txt, client_address): + """Sends 'txt', encapsulated in an OSCMessage to the default 'info_prefix' OSC-addres. + Message is sent to the given client_address, with the default 'return_port' overriding + the client_address' port, if defined. + """ + lines = txt.split('\n') + if len(lines) == 1: + msg = OSCMessage(self.info_prefix) + msg.append(lines[0]) + elif len(lines) > 1: + msg = OSCBundle(self.info_prefix) + for line in lines: + msg.append(line) + else: + return + + if self.return_port: + client_address = (client_address[0], self.return_port) + + self.client.sendto(msg, client_address) + + ### + # Message-Handler callback functions + ### + + def handle_error(self, request, client_address): + """Handle an exception in the Server's callbacks gracefully. + Writes the error to sys.stderr and, if the error_prefix (see setSrvErrorPrefix()) is set, + sends the error-message as reply to the client + """ + (e_type, e) = sys.exc_info()[:2] + self.printErr("%s on request from %s: %s" % (e_type.__name__, getUrlStr(client_address), str(e))) + + if self.print_tracebacks: + import traceback + traceback.print_exc() # XXX But this goes to stderr! + + if len(self.error_prefix): + self.sendOSCerror("%s: %s" % (e_type.__name__, str(e)), client_address) + + def noCallback_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler prints a "No callback registered to handle ..." message. + Returns None + """ + self.reportErr("No callback registered to handle OSC-address '%s'" % addr, client_address) + + def msgPrinter_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler prints the received message. + Returns None + """ + txt = "OSCMessage '%s' from %s: " % (addr, getUrlStr(client_address)) + txt += str(data) + + self.printErr(txt) # strip trailing comma & space + + def serverInfo_handler(self, addr, tags, data, client_address): + """Example handler for OSCMessages. + All registerd handlers must accept these three arguments: + - addr (string): The OSC-address pattern of the received Message + (the 'addr' string has already been matched against the handler's registerd OSC-address, + but may contain '*'s & such) + - tags (string): The OSC-typetags of the received message's arguments. (without the preceding comma) + - data (list): The OSCMessage's arguments + Note that len(tags) == len(data) + - client_address ((host, port) tuple): the host & port this message originated from. + + a Message-handler function may return None, but it could also return an OSCMessage (or OSCBundle), + which then gets sent back to the client. + + This handler returns a reply to the client, which can contain various bits of information + about this server, depending on the first argument of the received OSC-message: + - 'help' | 'info' : Reply contains server type & version info, plus a list of + available 'commands' understood by this handler + - 'list' | 'ls' : Reply is a bundle of 'address ' messages, listing the server's + OSC address-space. + - 'clients' | 'targets' : Reply is a bundle of 'target osc://:[] [] [...]' + messages, listing the local Client-instance's subscribed remote clients. + """ + if len(data) == 0: + return None + + cmd = data.pop(0) + + reply = None + if cmd in ('help', 'info'): + reply = OSCBundle(self.info_prefix) + reply.append(('server', str(self))) + reply.append(('info_command', "ls | list : list OSC address-space")) + reply.append(('info_command', "clients | targets : list subscribed clients")) + elif cmd in ('ls', 'list'): + reply = OSCBundle(self.info_prefix) + for addr in list(self.callbacks.keys()): + reply.append(('address', addr)) + elif cmd in ('clients', 'targets'): + if hasattr(self.client, 'getOSCTargetStrings'): + reply = OSCBundle(self.info_prefix) + for trg in self.client.getOSCTargetStrings(): + reply.append(('target',) + trg) + else: + cli_addr = self.client.address() + if cli_addr: + reply = OSCMessage(self.info_prefix) + reply.append(('target', "osc://%s/" % getUrlStr(cli_addr))) + else: + self.reportErr("unrecognized command '%s' in /info request from osc://%s. Try 'help'" % (cmd, getUrlStr(client_address)), client_address) + + return reply + + def _subscribe(self, data, client_address): + """Handle the actual subscription. the provided 'data' is concatenated together to form a + ':[] [] [...]' string, which is then passed to + parseUrlStr() & parseFilterStr() to actually retreive , , etc. + + This 'long way 'round' approach (almost) guarantees that the subscription works, + regardless of how the bits of the are encoded in 'data'. + """ + url = "" + have_port = False + for item in data: + if (isinstance(item,int)) and not have_port: + url += ":%d" % item + have_port = True + elif isinstance(item,str): + url += item + + (addr, tail) = parseUrlStr(url) + (prefix, filters) = parseFilterStr(tail) + + if addr != None: + (host, port) = addr + if not host: + host = client_address[0] + if not port: + port = client_address[1] + addr = (host, port) + else: + addr = client_address + + self.client._setTarget(addr, prefix, filters) + + trg = self.client.getOSCTargetStr(addr) + if trg[0] != None: + reply = OSCMessage(self.info_prefix) + reply.append(('target',) + trg) + return reply + + def _unsubscribe(self, data, client_address): + """Handle the actual unsubscription. the provided 'data' is concatenated together to form a + ':[]' string, which is then passed to + parseUrlStr() to actually retreive , & . + + This 'long way 'round' approach (almost) guarantees that the unsubscription works, + regardless of how the bits of the are encoded in 'data'. + """ + url = "" + have_port = False + for item in data: + if (isinstance(item,int)) and not have_port: + url += ":%d" % item + have_port = True + elif isinstance(item,str): + url += item + + (addr, _) = parseUrlStr(url) + + if addr == None: + addr = client_address + else: + (host, port) = addr + if not host: + host = client_address[0] + if not port: + try: + (host, port) = self.client._searchHostAddr(host) + except NotSubscribedError: + port = client_address[1] + + addr = (host, port) + + try: + self.client._delTarget(addr) + except NotSubscribedError as e: + txt = "%s: %s" % (e.__class__.__name__, str(e)) + self.printErr(txt) + + reply = OSCMessage(self.error_prefix) + reply.append(txt) + return reply + + def subscription_handler(self, addr, tags, data, client_address): + """Handle 'subscribe' / 'unsubscribe' requests from remote hosts, + if the local Client supports this (i.e. OSCMultiClient). + + Supported commands: + - 'help' | 'info' : Reply contains server type & version info, plus a list of + available 'commands' understood by this handler + - 'list' | 'ls' : Reply is a bundle of 'target osc://:[] [] [...]' + messages, listing the local Client-instance's subscribed remote clients. + - '[subscribe | listen | sendto | target] [ ...] : Subscribe remote client/server at , + and/or set message-filters for messages being sent to the subscribed host, with the optional + arguments. Filters are given as OSC-addresses (or '*') prefixed by a '+' (send matching messages) or + a '-' (don't send matching messages). The wildcard '*', '+*' or '+/*' means 'send all' / 'filter none', + and '-*' or '-/*' means 'send none' / 'filter all' (which is not the same as unsubscribing!) + Reply is an OSCMessage with the (new) subscription; 'target osc://:[] [] [...]' + - '[unsubscribe | silence | nosend | deltarget] : Unsubscribe remote client/server at + If the given isn't subscribed, a NotSubscribedError-message is printed (and possibly sent) + + The given to the subscribe/unsubscribe handler should be of the form: + '[osc://][][:][]', where any or all components can be omitted. + + If is not specified, the IP-address of the message's source is used. + If is not specified, the is first looked up in the list of subscribed hosts, and if found, + the associated port is used. + If is not specified and is not yet subscribed, the message's source-port is used. + If is specified on subscription, is prepended to the OSC-address of all messages + sent to the subscribed host. + If is specified on unsubscription, the subscribed host is only unsubscribed if the host, + port and prefix all match the subscription. + If is not specified on unsubscription, the subscribed host is unsubscribed if the host and port + match the subscription. + """ + if not isinstance(self.client, OSCMultiClient): + raise OSCServerError("Local %s does not support subsctiptions or message-filtering" % self.client.__class__.__name__) + + addr_cmd = addr.split('/')[-1] + + if len(data): + if data[0] in ('help', 'info'): + reply = OSCBundle(self.info_prefix) + reply.append(('server', str(self))) + reply.append(('subscribe_command', "ls | list : list subscribed targets")) + reply.append(('subscribe_command', "[subscribe | listen | sendto | target] [ ...] : subscribe to messages, set filters")) + reply.append(('subscribe_command', "[unsubscribe | silence | nosend | deltarget] : unsubscribe from messages")) + return reply + + if data[0] in ('ls', 'list'): + reply = OSCBundle(self.info_prefix) + for trg in self.client.getOSCTargetStrings(): + reply.append(('target',) + trg) + return reply + + if data[0] in ('subscribe', 'listen', 'sendto', 'target'): + return self._subscribe(data[1:], client_address) + + if data[0] in ('unsubscribe', 'silence', 'nosend', 'deltarget'): + return self._unsubscribe(data[1:], client_address) + + if addr_cmd in ('subscribe', 'listen', 'sendto', 'target'): + return self._subscribe(data, client_address) + + if addr_cmd in ('unsubscribe', 'silence', 'nosend', 'deltarget'): + return self._unsubscribe(data, client_address) + +class ForkingOSCServer(ForkingMixIn, OSCServer): + """An Asynchronous OSCServer. + This server forks a new process to handle each incoming request. + """ + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = ThreadingOSCRequestHandler + +class ThreadingOSCServer(ThreadingMixIn, OSCServer): + """An Asynchronous OSCServer. + This server starts a new thread to handle each incoming request. + """ + # set the RequestHandlerClass, will be overridden by ForkingOSCServer & ThreadingOSCServer + RequestHandlerClass = ThreadingOSCRequestHandler + +###### +# +# OSCError classes +# +###### + +class OSCError(Exception): + """Base Class for all OSC-related errors + """ + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + +class OSCClientError(OSCError): + """Class for all OSCClient errors + """ + pass + +class OSCServerError(OSCError): + """Class for all OSCServer errors + """ + pass + +class NoCallbackError(OSCServerError): + """This error is raised (by an OSCServer) when an OSCMessage with an 'unmatched' address-pattern + is received, and no 'default' handler is registered. + """ + def __init__(self, pattern): + """The specified 'pattern' should be the OSC-address of the 'unmatched' message causing the error to be raised. + """ + self.message = "No callback registered to handle OSC-address '%s'" % pattern + +class NotSubscribedError(OSCClientError): + """This error is raised (by an OSCMultiClient) when an attempt is made to unsubscribe a host + that isn't subscribed. + """ + def __init__(self, addr, prefix=None): + if prefix: + url = getUrlStr(addr, prefix) + else: + url = getUrlStr(addr, '') + + self.message = "Target osc://%s is not subscribed" % url + +###### +# +# OSC over streaming transport layers (usually TCP) +# +# Note from the OSC 1.0 specifications about streaming protocols: +# +# The underlying network that delivers an OSC packet is responsible for +# delivering both the contents and the size to the OSC application. An OSC +# packet can be naturally represented by a datagram by a network protocol such +# as UDP. In a stream-based protocol such as TCP, the stream should begin with +# an int32 giving the size of the first packet, followed by the contents of the +# first packet, followed by the size of the second packet, etc. +# +# The contents of an OSC packet must be either an OSC Message or an OSC Bundle. +# The first byte of the packet's contents unambiguously distinguishes between +# these two alternatives. +# +###### + +class OSCStreamRequestHandler(StreamRequestHandler, OSCAddressSpace): + """ This is the central class of a streaming OSC server. If a client + connects to the server, the server instantiates a OSCStreamRequestHandler + for each new connection. This is fundamentally different to a packet + oriented server which has a single address space for all connections. + This connection based (streaming) OSC server maintains an address space + for each single connection, because usually tcp server spawn a new thread + or process for each new connection. This would generate severe + multithreading synchronization problems when each thread would operate on + the same address space object. Therefore: To implement a streaming/TCP OSC + server a custom handler must be implemented which implements the + setupAddressSpace member in which it creates its own address space for this + very connection. This has been done within the testbench and can serve as + inspiration. + """ + def __init__(self, request, client_address, server): + """ Initialize all base classes. The address space must be initialized + before the stream request handler because the initialization function + of the stream request handler calls the setup member which again + requires an already initialized address space. + """ + self._txMutex = threading.Lock() + OSCAddressSpace.__init__(self) + StreamRequestHandler.__init__(self, request, client_address, server) + + def _unbundle(self, decoded): + """Recursive bundle-unpacking function""" + if decoded[0] != "#bundle": + self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.client_address) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def setup(self): + StreamRequestHandler.setup(self) + print("SERVER: New client connection.") + self.setupAddressSpace() + self.server._clientRegister(self) + + def setupAddressSpace(self): + """ Override this function to customize your address space. """ + pass + + def finish(self): + StreamRequestHandler.finish(self) + self.server._clientUnregister(self) + print("SERVER: Client connection handled.") + def _transmit(self, data): + sent = 0 + while sent < len(data): + tmp = self.connection.send(data[sent:]) + if tmp == 0: + return False + sent += tmp + return True + def _transmitMsg(self, msg): + """Send an OSC message over a streaming socket. Raises exception if it + should fail. If everything is transmitted properly, True is returned. If + socket has been closed, False. + """ + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + + try: + binary = msg.getBinary() + length = len(binary) + # prepend length of packet before the actual message (big endian) + len_big_endian = array.array('c', '\0' * 4) + struct.pack_into(">L", len_big_endian, 0, length) + len_big_endian = len_big_endian.tostring() + if self._transmit(len_big_endian) and self._transmit(binary): + return True + return False + except socket.error as e: + if e[0] == errno.EPIPE: # broken pipe + return False + raise e + + def _receive(self, count): + """ Receive a certain amount of data from the socket and return it. If the + remote end should be closed in the meanwhile None is returned. + """ + chunk = self.connection.recv(count) + if not chunk or len(chunk) == 0: + return None + while len(chunk) < count: + tmp = self.connection.recv(count - len(chunk)) + if not tmp or len(tmp) == 0: + return None + chunk = chunk + tmp + return chunk + + def _receiveMsg(self): + """ Receive OSC message from a socket and decode. + If an error occurs, None is returned, else the message. + """ + # get OSC packet size from stream which is prepended each transmission + chunk = self._receive(4) + if chunk == None: + print("SERVER: Socket has been closed.") + return None + # extract message length from big endian unsigned long (32 bit) + slen = struct.unpack(">L", chunk)[0] + # receive the actual message + chunk = self._receive(slen) + if chunk == None: + print("SERVER: Socket has been closed.") + return None + # decode OSC data and dispatch + msg = decodeOSC(chunk) + if msg == None: + raise OSCError("SERVER: Message decoding failed.") + return msg + + def handle(self): + """ + Handle a connection. + """ + # set socket blocking to avoid "resource currently not available" + # exceptions, because the connection socket inherits the settings + # from the listening socket and this times out from time to time + # in order to provide a way to shut the server down. But we want + # clean and blocking behaviour here + self.connection.settimeout(None) + + print("SERVER: Entered server loop") + try: + while True: + decoded = self._receiveMsg() + if decoded == None: + return + elif len(decoded) <= 0: + # if message decoding fails we try to stay in sync but print a message + print("OSC stream server: Spurious message received.") + continue + + self.replies = [] + self._unbundle(decoded) + + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + # no replies, continue receiving + continue + self._txMutex.acquire() + txOk = self._transmitMsg(msg) + self._txMutex.release() + if not txOk: + break + + except socket.error as e: + if e[0] == errno.ECONNRESET: + # if connection has been reset by client, we do not care much + # about it, we just assume our duty fullfilled + print("SERVER: Connection has been reset by peer.") + else: + raise e + + def sendOSC(self, oscData): + """ This member can be used to transmit OSC messages or OSC bundles + over the client/server connection. It is thread save. + """ + self._txMutex.acquire() + result = self._transmitMsg(oscData) + self._txMutex.release() + return result + +""" TODO Note on threaded unbundling for streaming (connection oriented) +transport: + +Threaded unbundling as implemented in ThreadingOSCServer must be implemented in +a different way for the streaming variant, because contrary to the datagram +version the streaming handler is instantiated only once per connection. This +leads to the problem (if threaded unbundling is implemented as in OSCServer) +that all further message reception is blocked until all (previously received) +pending messages are processed. + +Each StreamRequestHandler should provide a so called processing queue in which +all pending messages or subbundles are inserted to be processed in the future). +When a subbundle or message gets queued, a mechanism must be provided that +those messages get invoked when time asks for them. There are the following +opportunities: + - a timer is started which checks at regular intervals for messages in the + queue (polling - requires CPU resources) + - a dedicated timer is started for each message (requires timer resources) +""" + +class OSCStreamingServer(TCPServer): + """ A connection oriented (TCP/IP) OSC server. + """ + + # define a socket timeout, so the serve_forever loop can actually exit. + # with 2.6 and server.shutdown this wouldn't be necessary + socket_timeout = 1 + + # this is the class which handles a new connection. Override this for a + # useful customized server. See the testbench for an example + RequestHandlerClass = OSCStreamRequestHandler + + def __init__(self, address): + """Instantiate an OSCStreamingServer. + - server_address ((host, port) tuple): the local host & UDP-port + the server listens for new connections. + """ + self._clientList = [] + self._clientListMutex = threading.Lock() + TCPServer.__init__(self, address, self.RequestHandlerClass) + self.socket.settimeout(self.socket_timeout) + + def serve_forever(self): + """Handle one request at a time until server is closed. + Had to add this since 2.5 does not support server.shutdown() + """ + self.running = True + while self.running: + self.handle_request() # this times-out when no data arrives. + + def start(self): + """ Start the server thread. """ + self._server_thread = threading.Thread(target=self.serve_forever) + self._server_thread.setDaemon(True) + self._server_thread.start() + + def stop(self): + """ Stop the server thread and close the socket. """ + self.running = False + self._server_thread.join() + self.server_close() + # 2.6 only + #self.shutdown() + + def _clientRegister(self, client): + """ Gets called by each request/connection handler when connection is + established to add itself to the client list + """ + self._clientListMutex.acquire() + self._clientList.append(client) + self._clientListMutex.release() + + def _clientUnregister(self, client): + """ Gets called by each request/connection handler when connection is + lost to remove itself from the client list + """ + self._clientListMutex.acquire() + self._clientList.remove(client) + self._clientListMutex.release() + + def broadcastToClients(self, oscData): + """ Send OSC message or bundle to all connected clients. """ + result = True + for client in self._clientList: + result = result and client.sendOSC(oscData) + return result + +class OSCStreamingServerThreading(ThreadingMixIn, OSCStreamingServer): + pass + """ Implements a server which spawns a separate thread for each incoming + connection. Care must be taken since the OSC address space is for all + the same. + """ + +class OSCStreamingClient(OSCAddressSpace): + """ OSC streaming client. + A streaming client establishes a connection to a streaming server but must + be able to handle replies by the server as well. To accomplish this the + receiving takes place in a secondary thread, because no one knows if we + have to expect a reply or not, i.e. synchronous architecture doesn't make + much sense. + Replies will be matched against the local address space. If message + handlers access code of the main thread (where the client messages are sent + to the server) care must be taken e.g. by installing sychronization + mechanisms or by using an event dispatcher which can handle events + originating from other threads. + """ + # set outgoing socket buffer size + sndbuf_size = 4096 * 8 + rcvbuf_size = 4096 * 8 + + def __init__(self): + self._txMutex = threading.Lock() + OSCAddressSpace.__init__(self) + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.sndbuf_size) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, self.rcvbuf_size) + self.socket.settimeout(1.0) + self._running = False + + def _receiveWithTimeout(self, count): + chunk = str() + while len(chunk) < count: + try: + tmp = self.socket.recv(count - len(chunk)) + except socket.timeout: + if not self._running: + print("CLIENT: Socket timed out and termination requested.") + return None + else: + continue + except socket.error as e: + if e[0] == errno.ECONNRESET: + print("CLIENT: Connection reset by peer.") + return None + else: + raise e + if not tmp or len(tmp) == 0: + print("CLIENT: Socket has been closed.") + return None + chunk = chunk + tmp + return chunk + def _receiveMsgWithTimeout(self): + """ Receive OSC message from a socket and decode. + If an error occurs, None is returned, else the message. + """ + # get OSC packet size from stream which is prepended each transmission + chunk = self._receiveWithTimeout(4) + if not chunk: + return None + # extract message length from big endian unsigned long (32 bit) + slen = struct.unpack(">L", chunk)[0] + # receive the actual message + chunk = self._receiveWithTimeout(slen) + if not chunk: + return None + # decode OSC content + msg = decodeOSC(chunk) + if msg == None: + raise OSCError("CLIENT: Message decoding failed.") + return msg + + def _receiving_thread_entry(self): + print("CLIENT: Entered receiving thread.") + self._running = True + while self._running: + decoded = self._receiveMsgWithTimeout() + if not decoded: + break + elif len(decoded) <= 0: + continue + + self.replies = [] + self._unbundle(decoded) + if len(self.replies) > 1: + msg = OSCBundle() + for reply in self.replies: + msg.append(reply) + elif len(self.replies) == 1: + msg = self.replies[0] + else: + continue + self._txMutex.acquire() + txOk = self._transmitMsgWithTimeout(msg) + self._txMutex.release() + if not txOk: + break + print("CLIENT: Receiving thread terminated.") + + def _unbundle(self, decoded): + if decoded[0] != "#bundle": + self.replies += self.dispatchMessage(decoded[0], decoded[1][1:], decoded[2:], self.socket.getpeername()) + return + + now = time.time() + timetag = decoded[1] + if (timetag > 0.) and (timetag > now): + time.sleep(timetag - now) + + for msg in decoded[2:]: + self._unbundle(msg) + + def connect(self, address): + self.socket.connect(address) + self.receiving_thread = threading.Thread(target=self._receiving_thread_entry) + self.receiving_thread.start() + + def close(self): + # let socket time out + self._running = False + self.receiving_thread.join() + self.socket.close() + + def _transmitWithTimeout(self, data): + sent = 0 + while sent < len(data): + try: + tmp = self.socket.send(data[sent:]) + except socket.timeout: + if not self._running: + print("CLIENT: Socket timed out and termination requested.") + return False + else: + continue + except socket.error as e: + if e[0] == errno.ECONNRESET: + print("CLIENT: Connection reset by peer.") + return False + else: + raise e + if tmp == 0: + return False + sent += tmp + return True + + def _transmitMsgWithTimeout(self, msg): + if not isinstance(msg, OSCMessage): + raise TypeError("'msg' argument is not an OSCMessage or OSCBundle object") + binary = msg.getBinary() + length = len(binary) + # prepend length of packet before the actual message (big endian) + len_big_endian = array.array('c', '\0' * 4) + struct.pack_into(">L", len_big_endian, 0, length) + len_big_endian = len_big_endian.tostring() + if self._transmitWithTimeout(len_big_endian) and self._transmitWithTimeout(binary): + return True + else: + return False + + def sendOSC(self, msg): + """Send an OSC message or bundle to the server. Returns True on success. + """ + self._txMutex.acquire() + txOk = self._transmitMsgWithTimeout(msg) + self._txMutex.release() + return txOk + + def __str__(self): + """Returns a string containing this Client's Class-name, software-version + and the remote-address it is connected to (if any) + """ + out = self.__class__.__name__ + out += " v%s.%s-%s" % version + addr = self.socket.getpeername() + if addr: + out += " connected to osc://%s" % getUrlStr(addr) + else: + out += " (unconnected)" + + return out + + def __eq__(self, other): + """Compare function. + """ + if not isinstance(other, self.__class__): + return False + + isequal = cmp(self.socket._sock, other.socket._sock) + if isequal and self.server and other.server: + return cmp(self.server, other.server) + + return isequal + + def __ne__(self, other): + """Compare function. + """ + return not self.__eq__(other) diff --git a/README.md b/README.md new file mode 100644 index 0000000..f630dc7 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +midiOSCesp +v0.1 + +More soon. \ No newline at end of file diff --git a/esp/boot.py b/esp/boot.py new file mode 100755 index 0000000..8994442 --- /dev/null +++ b/esp/boot.py @@ -0,0 +1,10 @@ +# This file is executed on every boot (including wake-boot from deepsleep) +#import esp +#esp.osdebug(None) +import uos, machine +#uos.dupterm(None, 1) # disable REPL on UART(0) +import gc +#import webrepl +#webrepl.start() + +gc.collect() diff --git a/esp/leds.py b/esp/leds.py new file mode 100644 index 0000000..eae1124 --- /dev/null +++ b/esp/leds.py @@ -0,0 +1,62 @@ +import time +import machine, neopixel +lednumber = 8 + +np = neopixel.NeoPixel(machine.Pin(14), lednumber) + +def demo(np): + n = np.n + + # cycle + for i in range(4 * n): + for j in range(n): + np[j] = (0, 0, 0) + np[i % n] = (255, 255, 255) + np.write() + time.sleep_ms(25) + + # bounce + for i in range(4 * n): + for j in range(n): + np[j] = (0, 0, 128) + if (i // n) % 2 == 0: + np[i % n] = (0, 0, 0) + else: + np[n - 1 - (i % n)] = (0, 0, 0) + np.write() + time.sleep_ms(60) + + # fade in/out + for i in range(0, 4 * 256, 8): + for j in range(n): + if (i // 256) % 2 == 0: + val = i & 0xff + else: + val = 255 - (i & 0xff) + np[j] = (val, 0, 0) + np.write() + + # clear + for i in range(n): + np[i] = (0, 0, 0) + np.write() + +def npcolor(r, g, b): + + for i in range(lednumber): + np[i] = (r, g, b) + np.write() + +def npcycle(): + + for i in range(2 * lednumber): + for j in range(lednumber): + np[j] = (0, 0, 0) + np[i % lednumber] = (126, 20, 126) + np.write() + time.sleep_ms(150) + +npcolor(0,0,0) +npcycle() +npcolor(0,0,0) +#demo(np) \ No newline at end of file diff --git a/esp/lserver.py b/esp/lserver.py new file mode 100644 index 0000000..9d1acb6 --- /dev/null +++ b/esp/lserver.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +import sys, time +import socket +from uosc.server import run_server, split_oscstr, parse_message, parse_bundle +from uosc.client import send +from machine import Pin +import mynetconf as netconf +import machine, neopixel + + +print('Loading OSC server module...') + + +MAX_DGRAM_SIZE = 1472 + +OSCport = 9001 + +ledpin = Pin(2, Pin.OUT) +stripin = Pin(14, Pin.OUT) +ESPledstate = False +n = 8 + + +np = neopixel.NeoPixel(stripin, n) + + +def ESPledon(): + global ESPledstate + + ledpin.value(0) # LED ON + ESPledstate = True + +def ESPledoff(): + global ESPledstate + + ledpin.value(1) # LED OFF + ESPledstate = False + +ESPledoff() + +def npcycle(): + + for i in range(2 * n): + for j in range(n): + np[j] = (0, 0, 0) + np[i % n] = (126, 20, 126) + np.write() + time.sleep_ms(150) + +''' +def npcycle2(r, g, b, wait): + + print(r,g,b,wait) + for i in range(2 * n): + for j in range(n): + np[j] = (0, 0, 0) + np[int(i % n)] = (r, g, b) + #print(np) + np.write() + print() + for count in range(n): + print(i%n) + time.sleep(wait) +''' + +def npcolor(r, g, b): + + for i in range(n): + np[i] = (r, g, b) + np.write() + +def npbounce(): + + for i in range(4 * n): + for j in range(n): + np[j] = (0, 0, 128) + if (i // n) % 2 == 0: + np[i % n] = (0, 0, 0) + else: + np[n - 1 - (i % n)] = (0, 0, 0) + np.write() + time.sleep_ms(60) + + + +def OSCHandler(t, msg): + + print() + print("OSCHandler") + print("OSC address:", msg[0]) # /on + print("Type tags:", msg[1]) # 'i' + print("Arguments:", msg[2]) # (1,) + print() + + # /noteon midichannel note velocity + if msg[0] == "/noteon": + print("NOTEON channel", msg[2][0], "note",msg[2][1], "velocity", msg[2][2]) + np[int(msg[2][1]%n)] = (255, 0, 153) + np.write() + + + # /noteoff midichannel note + + if msg[0] =='/noteoff': + print("NOTEOFF channel", msg[2][0], "note",msg[2][1]) + np[int(msg[2][1]%n)] = (0, 0, 0) + np.write() + + +def handle_rawosc(data, src, strict=False): + try: + head, _ = split_oscstr(data, 0) + if __debug__: print("head", head) + + if head.startswith('/'): + messages = [(-1, parse_message(data, strict))] + elif head == '#bundle': + messages = parse_bundle(data, strict) + except Exception as exc: + if __debug__: + pass + return + + try: + for timetag, (oscaddr, tags, args) in messages: + if __debug__: + print("OSC address: %s" % oscaddr) + print("OSC type tags: %r" % tags) + print("OSC arguments: %r" % (args,)) + + print("Dispatching", timetag, (oscaddr, tags, args, src)) + OSCHandler(timetag, (oscaddr, tags, args, src)) + + except Exception as exc: + print("Exception in OSC handler: %s", exc) + + + +def run_server(saddr, port): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if __debug__: print("Created OSC UDP server socket.") + + sock.bind((saddr, port)) + print("Listening for OSC messages on %s:%i.", saddr, port) + + try: + while True: + data, caddr = sock.recvfrom(MAX_DGRAM_SIZE) + if __debug__: print("Server received", len(data), "bytes from",caddr) + handle_rawosc(data, caddr) + print() + finally: + sock.close() + print("Bye!") + + + +def main(ip): + + #import time + ESPledon() + print("Ledserver main IP",ip) + time.sleep(1) + npcolor(0,0,0) + #npcycle(255, 0, 0, 2) + #npbounce() + npcycle() + npcolor(0,0,0) + + start = time.time() + + try: + run_server("0.0.0.0", int(netconf.OSCin)) + except KeyboardInterrupt: + pass + + ESPledoff() + +if __name__ == '__main__': + main(0) \ No newline at end of file diff --git a/esp/main.py b/esp/main.py new file mode 100755 index 0000000..5093bf5 --- /dev/null +++ b/esp/main.py @@ -0,0 +1,12 @@ +''' +midiOSCesp v0.1b ESP8266 side +''' + +from wlan import do_connect +import mynetconf as netconf +print('connection') +ip = do_connect(netconf.ssid, netconf.password) +print("in main IP :",ip) + +from lserver import main +main(ip) diff --git a/esp/netconf.py b/esp/netconf.py new file mode 100644 index 0000000..1e3472c --- /dev/null +++ b/esp/netconf.py @@ -0,0 +1,8 @@ +ssid = 'ssidname' +password = 'password' +ip = '192.168.2.142' +mask = '255.255.255.0' +gateway = '192.168.2.254' +dns = '1.1.1.1' +OSCin = 9001 +OSCout = 9002 \ No newline at end of file diff --git a/esp/uosc/__init__.py b/esp/uosc/__init__.py new file mode 100755 index 0000000..ceec9ce --- /dev/null +++ b/esp/uosc/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +"""A minimal OSC client and server library for MicroPython. + +To use it with the unix port of MicroPython, install the required modules from +the micropython-lib: + + $ for mod in argparse ffilib logging socket struct; do + micropython -m upip install micropython-$mod + done + +""" diff --git a/esp/uosc/__main__.py b/esp/uosc/__main__.py new file mode 100755 index 0000000..ac5aa95 --- /dev/null +++ b/esp/uosc/__main__.py @@ -0,0 +1,34 @@ +#!/usr/bin/env micropython +# -*- coding: utf-8 -*- + +import argparse +import logging +import sys + +from uosc.server import run_server + + +DEFAULT_ADDRESS = '0.0.0.0' +DEFAULT_PORT = 9001 + + +def main(args=None): + ap = argparse.ArgumentParser() + ap.add_argument('-v', '--verbose', action="store_true", + help="Enable debug logging") + ap.add_argument('-a', '--address', default=DEFAULT_ADDRESS, + help="OSC server address (default: %s)" % DEFAULT_ADDRESS) + ap.add_argument('-p', '--port', type=int, default=DEFAULT_PORT, + help="OSC server port (default: %s)" % DEFAULT_PORT) + + args = ap.parse_args(args if args is not None else sys.argv[1:]) + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) + + try: + run_server(args.address, int(args.port)) + except KeyboardInterrupt: + pass + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:]) or 0) diff --git a/esp/uosc/client.py b/esp/uosc/client.py new file mode 100755 index 0000000..d1a455e --- /dev/null +++ b/esp/uosc/client.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# +# uosc/client.py +# +"""Simple OSC client.""" + +import socket + +try: + from ustruct import pack +except ImportError: + from struct import pack + +from uosc.common import Bundle, to_frac + + +if isinstance('', bytes): + have_bytes = False + unicodetype = unicode # noqa +else: + have_bytes = True + unicodetype = str + +TYPE_MAP = { + int: 'i', + float: 'f', + bytes: 'b', + bytearray: 'b', + unicodetype: 's', + True: 'T', + False: 'F', + None: 'N', +} + + +def pack_addr(addr): + """Pack a (host, port) tuple into the format expected by socket methods.""" + if isinstance(addr, (bytes, bytearray)): + return addr + + if len(addr) != 2: + raise NotImplementedError("Only IPv4/v6 supported") + + addrinfo = socket.getaddrinfo(addr[0], addr[1]) + return addrinfo[0][4] + + +def pack_timetag(t): + """Pack an OSC timetag into 64-bit binary blob.""" + return pack('>II', *to_frac(t)) + + +def pack_string(s, encoding='utf-8'): + """Pack a string into a binary OSC string.""" + if isinstance(s, unicodetype): + s = s.encode(encoding) + assert all((i if have_bytes else ord(i)) < 128 for i in s), ( + "OSC strings may only contain ASCII chars.") + + slen = len(s) + return s + b'\0' * (((slen + 4) & ~0x03) - slen) + + +def pack_blob(b, encoding='utf-8'): + """Pack a bytes, bytearray or tuple/list of ints into a binary OSC blob.""" + if isinstance(b, (tuple, list)): + b = bytearray(b) + elif isinstance(b, unicodetype): + b = b.encode(encoding) + + blen = len(b) + b = pack('>I', blen) + bytes(b) + return b + b'\0' * (((blen + 3) & ~0x03) - blen) + + +def pack_bundle(bundle): + """Return bundle data packed into a binary string.""" + data = [] + for msg in bundle: + if isinstance(msg, Bundle): + msg = pack_bundle(msg) + elif isinstance(msg, tuple): + msg = create_message(*msg) + + data.append(pack('>I', len(msg)) + msg) + + return b'#bundle\0' + pack_timetag(bundle.timetag) + b''.join(data) + + +def pack_midi(val): + assert not isinstance(val, unicodetype), ( + "Value with tag 'm' or 'r' must be bytes, bytearray or a sequence of " + "ints, not %s" % unicodetype) + if not have_bytes and isinstance(val, str): + val = (ord(c) for c in val) + + return pack('BBBB', *tuple(val)) + + +def create_message(address, *args): + """Create an OSC message with given address pattern and arguments. + + The OSC types are either inferred from the Python types of the arguments, + or you can pass arguments as 2-item tuples with the OSC typetag as the + first item and the argument value as the second. Python objects are mapped + to OSC typetags as follows: + + * ``int``: i + * ``float``: f + * ``str``: s + * ``bytes`` / ``bytearray``: b + * ``None``: N + * ``True``: T + * ``False``: F + + If you want to encode a Python object to another OSC type, you have to pass + a ``(typetag, data)`` tuple, where ``data`` must be of the appropriate type + according to the following table: + + * c: ``str`` of length 1 + * h: ``int`` + * d: ``float`` + * I: ``None`` (unused) + * m: ``tuple / list`` of 4 ``int``s or ``bytes / bytearray`` of length 4 + * r: same as 'm' + * t: OSC timetag as as ``int / float`` seconds since the NTP epoch + * S: ``str`` + + """ + assert address.startswith('/'), "Address pattern must start with a slash." + + data = [] + types = [','] + + for arg in args: + type_ = type(arg) + + if isinstance(arg, tuple): + typetag, arg = arg + else: + typetag = TYPE_MAP.get(type_) or TYPE_MAP.get(arg) + + if typetag in 'ifd': + data.append(pack('>' + typetag, arg)) + elif typetag in 'sS': + data.append(pack_string(arg)) + elif typetag == 'b': + data.append(pack_blob(arg)) + elif typetag in 'rm': + data.append(pack_midi(arg)) + elif typetag == 'c': + data.append(pack('>I', ord(arg))) + elif typetag == 'h': + data.append(pack('>q', arg)) + elif typetag == 't': + data.append(pack_timetag(arg)) + elif typetag not in 'IFNT': + raise TypeError("Argument of type '%s' not supported." % type_) + + types.append(typetag) + + return pack_string(address) + pack_string(''.join(types)) + b''.join(data) + + +class Client: + def __init__(self, host, port=None): + if port is None: + if isinstance(host, (list, tuple)): + host, port = host + else: + port = host + host = '127.0.0.1' + + self.dest = pack_addr((host, port)) + self.sock = None + + def send(self, msg, *args, **kw): + dest = pack_addr(kw.get('dest', self.dest)) + + if not self.sock: + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + if isinstance(msg, Bundle): + msg = pack_bundle(msg) + elif args or isinstance(msg, unicodetype): + msg = create_message(msg, *args) + + self.sock.sendto(msg, dest) + + def close(self): + if self.sock: + self.sock.close() + self.sock = None + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +def send(dest, address, *args): + with Client(dest) as client: + client.send(address, *args) diff --git a/esp/uosc/common.py b/esp/uosc/common.py new file mode 100755 index 0000000..8a26d57 --- /dev/null +++ b/esp/uosc/common.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# uosc/common.py +# +"""OSC message parsing and building functions.""" + +try: + from time import time +except ImportError: + from utime import time + + +# UNIX_EPOCH = datetime.date(*time.gmtime(0)[0:3]) +# NTP_EPOCH = datetime.date(1900, 1, 1) +# NTP_DELTA = (UNIX_EPOCH - NTP_EPOCH).days * 24 * 3600 +NTP_DELTA = 2208988800 +ISIZE = 4294967296 # 2**32 + + +class Impulse: + pass + + +class Bundle: + """Container for an OSC bundle.""" + + def __init__(self, *items): + """Create bundle from given OSC timetag and messages/sub-bundles. + + An OSC timetag can be given as the first positional argument, and must + be an int or float of seconds since the NTP epoch (1990-01-01 00:00). + It defaults to the current time. + + Pass in messages or bundles via positional arguments as binary data + (bytes as returned by ``create_message`` resp. ``Bundle.pack``) or as + ``Bundle`` instances or (address, *args) tuples. + + """ + if items and isinstance(items[0], (int, float)): + self.timetag = items[0] + items = items[1:] + else: + self.timetag = time() + NTP_DELTA + + self._items = list(items) + + def add(self, *items): + self._items.extend(list(items)) + + def __iter__(self): + return iter(self._items) + + +def to_frac(t): + """Return seconds and fractional part of NTP timestamp as 2-item tuple.""" + sec = int(t) + return sec, int(abs(t - sec) * ISIZE) + + +def to_time(sec, frac): + """Return NTP timestamp from integer seconds and fractional part.""" + return sec + float(frac) / ISIZE diff --git a/esp/uosc/server.py b/esp/uosc/server.py new file mode 100755 index 0000000..0cca49e --- /dev/null +++ b/esp/uosc/server.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# +# uosc/server.py +# +"""A minimal OSC UDP server.""" + + +import socket + +try: + from ustruct import unpack +except ImportError: + from struct import unpack + +from uosc.common import Impulse, to_time + +#if __debug__: +# from uosc.socketutil import get_hostport + + + +MAX_DGRAM_SIZE = 1472 + + +def split_oscstr(msg, offset): + end = msg.find(b'\0', offset) + return msg[offset:end].decode('utf-8'), (end + 4) & ~0x03 + + +def split_oscblob(msg, offset): + start = offset + 4 + size = unpack('>I', msg[offset:start])[0] + return msg[start:start + size], (start + size + 4) & ~0x03 + + +def parse_timetag(msg, offset): + """Parse an OSC timetag from msg at offset.""" + return to_time(unpack('>II', msg[offset:offset + 4])) + + +def parse_message(msg, strict=False): + args = [] + addr, ofs = split_oscstr(msg, 0) + + if not addr.startswith('/'): + raise ValueError("OSC address pattern must start with a slash.") + + # type tag string must start with comma (ASCII 44) + if ofs < len(msg) and msg[ofs:ofs + 1] == b',': + tags, ofs = split_oscstr(msg, ofs) + tags = tags[1:] + else: + errmsg = "Missing/invalid OSC type tag string." + if strict: + raise ValueError(errmsg) + else: + print(errmsg + ' Ignoring arguments.') + tags = '' + + for typetag in tags: + size = 0 + + if typetag in 'ifd': + size = 8 if typetag == 'd' else 4 + args.append(unpack('>' + typetag, msg[ofs:ofs + size])[0]) + elif typetag in 'sS': + s, ofs = split_oscstr(msg, ofs) + args.append(s) + elif typetag == 'b': + s, ofs = split_oscblob(msg, ofs) + args.append(s) + elif typetag in 'rm': + size = 4 + args.append(unpack('BBBB', msg[ofs:ofs + size])) + elif typetag == 'c': + size = 4 + args.append(chr(unpack('>I', msg[ofs:ofs + size])[0])) + elif typetag == 'h': + size = 8 + args.append(unpack('>q', msg[ofs:ofs + size])[0]) + elif typetag == 't': + size = 8 + args.append(parse_timetag(msg, ofs)) + elif typetag in 'TFNI': + args.append({'T': True, 'F': False, 'I': Impulse}.get(typetag)) + else: + raise ValueError("Type tag '%s' not supported." % typetag) + + ofs += size + + return (addr, tags, tuple(args)) + + +def parse_bundle(bundle, strict=False): + """Parse a binary OSC bundle. + + Returns a generator which walks over all contained messages and bundles + recursively, depth-first. Each item yielded is a (timetag, message) tuple. + + """ + if not bundle.startswith(b'#bundle\0'): + raise TypeError("Bundle must start with b'#bundle\\0'.") + + ofs = 16 + timetag = to_time(*unpack('>II', bundle[8:ofs])) + + while True: + if ofs >= len(bundle): + break + + size = unpack('>I', bundle[ofs:ofs + 4])[0] + element = bundle[ofs + 4:ofs + 4 + size] + ofs += size + 4 + + if element.startswith(b'#bundle'): + for el in parse_bundle(element): + yield el + else: + yield timetag, parse_message(element, strict) + + +def handle_osc(data, src, dispatch=None, strict=False): + try: + head, _ = split_oscstr(data, 0) + + if head.startswith('/'): + messages = [(-1, parse_message(data, strict))] + elif head == '#bundle': + messages = parse_bundle(data, strict) + except Exception as exc: + pass + #if __debug__: + # print(data) + return + + try: + for timetag, (oscaddr, tags, args) in messages: + if __debug__: + print("OSC address: %s" % oscaddr) + print("OSC type tags: %r" % tags) + print("OSC arguments: %r" % (args,)) + + if dispatch: + dispatch(timetag, (oscaddr, tags, args, src)) + except Exception as exc: + print("Exception in OSC handler: %s", exc) + + +def run_server(saddr, port, handler=handle_osc): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if __debug__: print("Created OSC UDP server socket.") + + sock.bind((saddr, port)) + print("Listening for OSC messages on %s:%i.", saddr, port) + + try: + while True: + data, caddr = sock.recvfrom(MAX_DGRAM_SIZE) + handler(data, caddr) + finally: + sock.close() + print("Bye!") diff --git a/esp/uosc/socketutil.py b/esp/uosc/socketutil.py new file mode 100755 index 0000000..3a7e32c --- /dev/null +++ b/esp/uosc/socketutil.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# +# uosc/socketutil.py +# + +import socket + + +INET_ADDRSTRLEN = 16 +INET6_ADDRSTRLEN = 46 + +inet_ntoa = getattr(socket, 'inet_ntoa', None) +if not inet_ntoa: + import ffilib + inet_ntoa = ffilib.libc().func("s", "inet_ntoa", "p") + + +inet_ntop = getattr(socket, 'inet_ntop', None) +if not inet_ntop: + import ffilib + _inet_ntop = ffilib.libc().func("s", "inet_ntop", "iPpi") + + def inet_ntop(af, addr): + buf = bytearray(INET_ADDRSTRLEN if af == socket.AF_INET else + INET6_ADDRSTRLEN) + res = _inet_ntop(af, addr, buf, INET_ADDRSTRLEN) + return res + + +def get_hostport(addr): + if isinstance(addr, tuple): + return addr + + af, addr, port = socket.sockaddr(addr) + return inet_ntop(af, addr), port diff --git a/esp/uosc/threadedclient.py b/esp/uosc/threadedclient.py new file mode 100755 index 0000000..579f6bd --- /dev/null +++ b/esp/uosc/threadedclient.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +"""OSC client running in a separate thread. + +Communicates with the main thread via a queue. Provides the same API as the +non-threaded client, with a few threading-related extensions: + + from uosc.threadedclient import ThreadedClient + + # start=True starts the thread immediately + osc = ThreadedClient('192.168.0.42', 9001, start=True) + + # if the OSC message can not placed in the queue within timeout + # raises a queue.Full error + osc.send('/pi', 3.14159, timeout=1.0) + # Stops and joins the thread and closes the client socket + osc.close() + +""" + +import logging +import threading + +try: + import queue +except ImportError: + import Queue as queue + +from uosc.client import Client + + +log = logging.getLogger(__name__) + + +class ThreadedClient(threading.Thread): + def __init__(self, host, port=None, start=False, timeout=3.0): + super(ThreadedClient, self).__init__() + self.host = host + self.port = port + self.timeout = timeout + self._q = queue.Queue() + + if start: + self.start() + + def run(self, *args, **kw): + self.client = Client((self.host, self.port)) + + while True: + msg = self._q.get() + if msg is None: + break + + addr, msg = msg + log.debug("Sending OSC msg %s, %r", addr, msg) + self.client.send(addr, *msg) + + self.client.close() + + def send(self, addr, *args, **kw): + self._q.put((addr, args), timeout=kw.get('timeout', self.timeout)) + + def close(self, **kw): + timeout = kw.get('timeout', self.timeout) + log.debug("Emptying send queue...") + + while True: + try: + self._q.get_nowait() + except queue.Empty: + break + + if self.is_alive(): + log.debug("Signalling OSC client thread to exit...") + self._q.put(None, timeout=timeout) + + log.debug("Joining OSC client thread...") + self.join(timeout) + + if self.is_alive(): + log.warning("OSC client thread still alive after join().") + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() diff --git a/esp/wlan.py b/esp/wlan.py new file mode 100755 index 0000000..3368733 --- /dev/null +++ b/esp/wlan.py @@ -0,0 +1,29 @@ +def do_connect(ssid, password, tries=5): + from network import WLAN, STA_IF + from time import sleep + import netconf + + print('Loading Wifi module...') + sta_if = WLAN(STA_IF) + + if not sta_if.isconnected(): + sta_if.active(True) + sta_if.connect(ssid, password) + + for i in range(tries): + print('Connecting to network (try {})...'.format(i+1)) + if sta_if.isconnected(): + sta_if.disconnect() + sta_if.status() + sta_if.ifconfig((netconf.ip, netconf.mask, netconf.gateway, netconf.dns)) + sta_if.connect() + curconf = sta_if.ifconfig() + print('network config:', curconf) + return curconf[0] + + sleep(1) + else: + print("Failed to connect in {} seconds.".format(tries)) + +if __name__ == '__main__': + do_connect(netconf.ssid, netconf.password) diff --git a/log.py b/log.py new file mode 100644 index 0000000..b86458a --- /dev/null +++ b/log.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +Log in color from + +https://stackoverflow.com/questions/287871/how-to-print-colored-text-in-terminal-in-python + +usage : + + import log + log.info("Hello World") + log.err("System Error") + +''' +HEADER = '\033[95m' +OKBLUE = '\033[94m' +OKGREEN = '\033[92m' +WARNING = '\033[93m' +FAIL = '\033[91m' +ENDC = '\033[0m' +BOLD = "\033[1m" + +def disable(): + HEADER = '' + OKBLUE = '' + OKGREEN = '' + WARNING = '' + FAIL = '' + ENDC = '' + +def infog( msg): + print(OKGREEN + msg + ENDC) + +def info( msg): + print(OKBLUE + msg + ENDC) + +def warn( msg): + print(WARNING + msg + ENDC) + +def err( msg): + print(FAIL + msg + ENDC) diff --git a/main.py b/main.py new file mode 100755 index 0000000..aae720e --- /dev/null +++ b/main.py @@ -0,0 +1,104 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- +''' +midiOSCesp v0.1b Computer side + +Hook to all midi ports, forward in OSC these midi events : + +/noteon MidiChannel, note, velocity +/noteoff MidiChannel, note +/rawcc MidiChannel, CCnumber, CCvalue +/clock +/start +/stop + +''' + +print("") +print("") +print("midiOSCesp Server") +print("v0.1b") + +import sys +import traceback +import os +import time + +from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, + PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE) + +#sys.path.append('libs/') +import midix + +#import socket +import types, json +import argparse + +import _thread, time + + +print ("") +print ("Arguments parsing if needed...") +argsparser = argparse.ArgumentParser(description="MidiOSCesp v0.1b commands help mode") +argsparser.add_argument('-verbose',help="Enable debug mode (disabled by default)", dest='verbose', action='store_true') +argsparser.set_defaults(verbose=False) + +args = argsparser.parse_args() + + +# Debug/verbose mode ? +if args.verbose == False: + print("Debug mode disabled") + debug = 0 +else: + print("Debug mode enabled") + debug = 1 + + +# +# Midi part +# + +print("Midi Configuration...") +# print("Midi Destination", nozmidi) + +midix.check() + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + + +# /cc cc number value +def cc(midichannel, ccnumber, value, mididest): + + if gstt.debug>0: + print(GetTime(),"Jamidi Sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest) + + midix.MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest) + + +# +# Running... +# + + +try: + + print(GetTime(),"midi2osc running forever...") + + while True: + time.sleep(1) + +except Exception: + traceback.print_exc() + + +# Gently stop on CTRL C + + +print("Fin de midiOSCesp.") + + + + diff --git a/midix.py b/midix.py new file mode 100644 index 0000000..6aaec1b --- /dev/null +++ b/midix.py @@ -0,0 +1,592 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +Midi3 light version for soundt/Jamidi/clapt +v0.7.0 + +Midi Handler : + +- Hook to the MIDI host +- Enumerate connected midi devices and spawn a process/device to handle incoming events + +by Sam Neurohack +from /team/laser + +Midi conversions from https://github.com/craffel/pretty-midi + +""" + +import time +from threading import Thread + +import rtmidi +from rtmidi.midiutil import open_midiinput +from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, + PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE, TIMING_CLOCK, SONG_CONTINUE, SONG_START, SONG_STOP) +import mido +from mido import MidiFile + +import traceback +import weakref +import sys +from sys import platform +import os +import re +from collections import deque +import log + + +from queue import Queue +from OSC3 import OSCServer, OSCClient, OSCMessage + + +print("") + +midiname = ["Name"] * 16 +midiport = [rtmidi.MidiOut() for i in range(16) ] + +OutDevice = [] +InDevice = [] + +midisync = True + +# max 16 midi port array + +midinputsname = ["Name"] * 16 +midinputsqueue = [Queue() for i in range(16) ] +midinputs = [] + +debug = 1 +#Mser = False + +MidInsNumber = 0 + +serverIP = "192.168.2.142" +OSCPORT = 9001 + +clock = mido.Message(type="clock") +start = mido.Message(type ="start") +stop = mido.Message(type ="stop") +ccontinue = mido.Message(type ="continue") +reset = mido.Message(type ="reset") +songpos = mido.Message(type ="songpos") + +#mode = "maxwell" + +''' +print "clock",clock) +print "start",start) +print "continue", ccontinue) +print "reset",reset) +print "sonpos",songpos) +''' + +try: + input = raw_input +except NameError: + # Python 3 + Exception = Exception + + +STATUS_MAP = { + 'noteon': NOTE_ON, + 'noteoff': NOTE_OFF, + 'programchange': PROGRAM_CHANGE, + 'controllerchange': CONTROLLER_CHANGE, + 'pitchbend': PITCH_BEND, + 'polypressure': POLY_PRESSURE, + 'channelpressure': CHANNEL_PRESSURE +} + + +def SendLeds(oscaddress,oscargs=''): + + oscmsg = OSCMessage() + oscmsg.setAddress(oscaddress) + oscmsg.append(oscargs) + + osclientlj = OSCClient() + osclientlj.connect((serverIP, OSCPORT)) + + #print("MIDI Aurora sending UI :", oscmsg, "to",TouchOSCIP,":",TouchOSCPort) + try: + osclientlj.sendto(oscmsg, (serverIP, OSCPORT)) + oscmsg.clearData() + except: + log.err('Connection to Aurora UI refused : died ?') + pass + #time.sleep(0.001 + + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + +# /cc cc number value +def cc(midichannel, ccnumber, value, mididest): + + if debug>0: + print("Midix sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest) + + MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest) + + + +# +# MIDI Startup and handling +# + +mqueue = Queue() +inqueue = Queue() +bpm = 0 +running = True +samples = deque() +last_clock = None + + +# +# Events from Generic MIDI Handling +# + +def MidinProcess(inqueue, portname): + + inqueue_get = inqueue.get + bpm = 0 + samples = deque() + last_clock = None + + while True: + time.sleep(0.001) + msg = inqueue_get() + #print("") + #print("Generic from", portname,"msg : ", msg) + + + # NOTE ON message on all midi channels + if NOTE_ON -1 < msg[0] < 160 and msg[2] !=0 : + + MidiChannel = msg[0]-144 + MidiNote = msg[1] + MidiVel = msg[2] + print() + print("NOTE ON :", MidiNote, 'velocity :', MidiVel, "Channel", MidiChannel) + #print("Midi in process send /aurora/noteon "+str(msg[1])+" "+str(msg[2])) + SendLeds("/noteon",[MidiChannel, msg[1], msg[2]]) + + ''' + # Sampler mode : note <63 launch snare.wav / note > 62 kick.wav + if MidiNote < 63 and MidiVel >0: + + if platform == 'darwin': + os.system("afplay snare.wav") + else: + os.system("aplay snare.wav") + + + if MidiNote > 62 and MidiVel >0: + + if platform == 'darwin': + os.system("afplay kick.wav") + else: + os.system("aplay kick.wav") + ''' + + # NOTE OFF or Note with 0 velocity on all midi channels + if NOTE_OFF -1 < msg[0] < 145 or (NOTE_OFF -1 < msg[0] < 160 and msg[2] == 0): + + print("NOTE_off :",NOTE_OFF) + if msg[0] > 143: + MidiChannel = msg[0]-144 + else: + MidiChannel = msg[0]-128 + #print("NOTE OFF :", MidiNote, "Channel", MidiChannel) + #print("Midi in process send /aurora/noteoff "+str(msg[1])) + SendLeds("/noteoff",[MidiChannel, msg[1]]) + + + # # CC on all Midi Channels + if CONTROLLER_CHANGE -1 < msg[0] < 192: + + MidiChannel = msg[0]-175 + print() + #print("channel", MidiChannel, "CC :", msg[1], msg[2]) + print("Midi in process send /aurora/rawcc "+str(msg[0]-175-1)+" "+str(msg[1])+" "+str(msg[2])) + SendLeds("/rawcc",[msg[0]-175-1, msg[1], msg[2]]) + + ''' + # MMO-3 Midi CC message CHANNEL 1 + if CONTROLLER_CHANGE -1 < msg[0] < 192: + print("channel 1 (MMO-3) CC :", msg[1], msg[2]) + print("Midi in process send /mmo3/cc/"+str(msg[1])+" "+str(msg[2])+" to WS") + WScom.send("/mmo3/cc/"+str(msg[1])+" "+str(msg[2])) + + + # OCS-2 Midi CC message CHANNEL 2 + if msg[0] == CONTROLLER_CHANGE+1: + print("channel 2 (OCS-2) CC :", msg[1], msg[2]) + + print("Midi in process send /ocs2/cc/"+str(msg[1])+" "+str(msg[2])+" to WS") + WScom.send("/ocs2/cc/"+str(msg[1])+" "+str(msg[2])) + ''' + + if msg[0] == TIMING_CLOCK: + now = time.time() + + if last_clock is not None: + samples.append(now - last_clock) + last_clock = now + + if len(samples) > 24: + samples.popleft() + + if len(samples) >= 2: + #bpm = 2.5 / (sum(samples) / len(samples)) + #print("%.2f bpm" % bpm) + + bpm = round(2.5 / (sum(samples) / len(samples))) # Against BPM lot very tiny change : + sync = True + # print("MIDI BPM", bpm) + + #print("Midi clock : BPM", bpm) + SendLeds("/clock",[]) + # SendAU("/aurora/bpm",[bpm]) + + + if msg[0] in (SONG_CONTINUE, SONG_START): + running = True + #print("START/CONTINUE received.") + #print("Midi in process send /aurora/start") + SendLeds("/start",[]) + + + if msg[0] == SONG_STOP: + running = False + #print("STOP received.") + #print("Midi in process send /aurora/stop") + SendLeds("/stop",[]) + + ''' + # other midi message + if msg[0] != NOTE_OFF and msg[0] != NOTE_ON and msg[0] != CONTROLLER_CHANGE: + pass + + print("from", portname,"other midi message") + MidiMsg(msg[0],msg[1],msg[2],mididest) + ''' + +#def NoteOn(note, color, mididest): +#https://pypi.org/project/python-rtmidi/0.3a/ +# NOTE_ON=#90 et NOTE_OFF=#80 on ajoute le channel (0 le premier) pour envoyer effectivement sur le channel +def NoteOn(note, color, mididest, midichannel=0): + global MidInsNumber + + if debug >0: + print("Sending", note, color, "to", mididest, "on channel", midichannel) + + for port in range(MidInsNumber): + + # To mididest + if midiname[port].find(mididest) == 0: + midiport[port].send_message([NOTE_ON+midichannel, note, color]) + + # To All + elif mididest == "all" and midiname[port].find(mididest) != 0: + midiport[port].send_message([NOTE_ON+midichannel, note, color]) + + + + +def NoteOff(note, mididest): + global MidInsNumber + + + for port in range(MidInsNumber): + + # To mididest + if midiname[port].find(mididest) != -1: + midiport[port].send_message([NOTE_OFF, note, 0]) + + # To All + elif mididest == "all" and midiname[port].find(mididest) == -1: + midiport[port].send_message([NOTE_OFF, note, 0]) + + + +# Generic call back : new msg forwarded to queue +class AddQueue(object): + def __init__(self, portname, port): + self.portname = portname + self.port = port + #print "AddQueue", port) + self._wallclock = time.time() + + def __call__(self, event, data=None): + message, deltatime = event + self._wallclock += deltatime + #print "inqueue : [%s] @%0.6f %r" % ( self.portname, self._wallclock, message)) + message.append(deltatime) + midinputsqueue[self.port].put(message) + + +# +# MIDI OUT Handling +# + +class OutObject(): + + _instances = set() + counter = 0 + + def __init__(self, name, kind, port): + + self.name = name + self.kind = kind + self.port = port + + self._instances.add(weakref.ref(self)) + OutObject.counter += 1 + + print("Adding OutDevice name", self.name, "kind", self.kind, "port", self.port) + + @classmethod + def getinstances(cls): + dead = set() + for ref in cls._instances: + obj = ref() + if obj is not None: + yield obj + else: + dead.add(ref) + cls._instances -= dead + + def __del__(self): + OutObject.counter -= 1 + + + +def OutConfig(): + global midiout, MidInsNumber + + # + if len(OutDevice) == 0: + print("") + log.info("MIDIout...") + print("List and attach to available devices on host with IN port :") + + # Display list of available midi IN devices on the host, create and start an OUT instance to talk to each of these Midi IN devices + midiout = rtmidi.MidiOut() + available_ports = midiout.get_ports() + + for port, name in enumerate(available_ports): + + midiname[port]=name + midiport[port].open_port(port) + #print ) + #print "New OutDevice [%i] %s" % (port, name)) + + OutDevice.append(OutObject(name, "generic", port)) + + #print "") + print(len(OutDevice), "Out devices") + #ListOutDevice() + MidInsNumber = len(OutDevice)+1 + +def ListOutDevice(): + + for item in OutObject.getinstances(): + + print(item.name) + +def FindOutDevice(name): + + port = -1 + for item in OutObject.getinstances(): + #print "searching", name, "in", item.name) + if name == item.name: + #print 'found port',item.port) + port = item.port + return port + + +def DelOutDevice(name): + + Outnumber = Findest(name) + print('deleting OutDevice', name) + + if Outnumber != -1: + print('found OutDevice', Outnumber) + delattr(OutObject, str(name)) + print("OutDevice", Outnumber,"was removed") + else: + print("OutDevice was not found") + + + +# +# MIDI IN Handling +# Create processing thread and queue for each device +# + +class InObject(): + + _instances = set() + counter = 0 + + def __init__(self, name, kind, port, rtmidi): + + self.name = name + self.kind = kind + self.port = port + self.rtmidi = rtmidi + self.queue = Queue() + + self._instances.add(weakref.ref(self)) + InObject.counter += 1 + + print("Adding InDevice name", self.name, "kind", self.kind, "port", self.port) + + @classmethod + def getinstances(cls): + dead = set() + for ref in cls._instances: + obj = ref() + if obj is not None: + yield obj + else: + dead.add(ref) + cls._instances -= dead + + def __del__(self): + InObject.counter -= 1 + + +def InConfig(): + + print("") + log.info("MIDIin...") + + + print("List and attach to available devices on host with OUT port :") + + if platform == 'darwin': + mido.set_backend('mido.backends.rtmidi/MACOSX_CORE') + + genericnumber = 0 + + for port, name in enumerate(mido.get_input_names()): + + + outport = FindOutDevice(name) + midinputsname[port]=name + + #print "name",name, "Port",port, "Outport", outport) + # print "midinames", midiname) + + #ListInDevice() + + try: + #print name, name.find("RtMidi output")) + if name.find("RtMidi output") > -1: + print("No thread started for device", name) + else: + portin = object + port_name = "" + portin, port_name = open_midiinput(outport) + + if midisync == True: + portin.ignore_types(timing=False) + + #midinputs.append(portin) + InDevice.append(InObject(name, "generic", outport, portin)) + + thread = Thread(target=MidinProcess, args=(midinputsqueue[port],port_name)) + thread.setDaemon(True) + thread.start() + + #print "Thread launched for midi port", port, "portname", port_name, "Inname", midiname.index(port_name) + #print "counter", InObject.counter + #midinputs[port].set_callback(AddQueue(name),midinputsqueue[port]) + #midinputs[port].set_callback(AddQueue(name)) + #genericnumber += 1 + InDevice[InObject.counter-1].rtmidi.set_callback(AddQueue(name,port)) + + except Exception: + traceback.print_exc() + + #print "") + print(InObject.counter, "In devices") + #ListInDevice() + + +def ListInDevice(): + + #print "known IN devices :" + for item in InObject.getinstances(): + + print(item.name) + print("") + +def FindInDevice(name): + + port = -1 + for item in InObject.getinstances(): + #print "searching", name, "in", item.name) + if name in item.name: + #print 'found port',item.port) + port = item.port + return port + + +def DelInDevice(name): + + Innumber = Findest(name) + print('deleting InDevice', name) + + if Innumber != -1: + print('found InDevice', Innumber) + delattr(InObject, str(name)) + print("InDevice", Innumber,"was removed") + else: + print("InDevice was not found") + + + +def End(): + global midiout + + #midiin.close_port() + midiout.close_port() + + +# mididest : all or specifiname, won't be sent to launchpad or Bhoreal. +def MidiMsg(midimsg, mididest): + + + desterror = -1 + + print("jamidi3 got midimsg", midimsg, "for", mididest) + + for port in range(len(OutDevice)): + # To mididest + if midiname[port].find(mididest) != -1: + if debug>0: + print("jamidi3 sending to name", midiname[port], "port", port, ":", midimsg) + midiport[port].send_message(midimsg) + desterror = 0 + + if desterror == -1: + print("mididest",mididest, ": ** This midi destination doesn't exists **") + + + +def listdevice(number): + + return midiname[number] + +def check(): + + OutConfig() + InConfig() + + +