***** Usage ***** .. toctree:: :maxdepth: 2 Introduction ============ What is osc4py3? ---------------- It is a fresh implementation of the Open Sound Control (OSC) protocol [#osc]_ for Python3. It tries to have a *“pythonic”* programming interface at top layers, and to support different scheduling schemes in lower layers to have efficient processing. .. [#osc] See http://opensoundcontrol.org/ for complete OSC documentation. What Python? ------------ The package targets Python3.2 or greater, see other OSC implementations for older Python versions (OSC.py, simpleosc, TxOSC). Why? ---- It was initially developed for use in a larger software as a background system communicating via OSC in separate threads, spending less possible time in main thread: sending OSC is just building messages/bundles objects and identifying targets, receiving OSC is just calling identified functions with OSC messages. All other operations, encoding and decoding, writing and reading, monitoring for communication channels availability, pattern matching messages addresses, etc, can be realized in other threads. On a multi-core CPU with multi-threading, these optimizations allow C code to run in parallel with communication operations, we gain milliseconds which are welcome in the project context. What's new? ----------- The whole package is really bigger and the underlying system is complex to understand. But, two base modules, :mod:`oscbuildparse` and :mod:`oscmethod` (see the modules documentation), are autonomous and can be used as is to provide: encoding and decoding of OSC messages and bundles, pattern matching and functions calling. As we start from scratch, we use latest information about OSC with support for advanced data types (adding new types is easy), and insert support for OSC extensions like address pattern compression or messages control. Complicated to use? ------------------- No. See the “Simple use” chapter below. It may be even easier to use than other Python OSC implementations. Don't mistake, the underlying processing is complex, but the high level API hide it. Quick OSC ========= `Open Sound Control `_ is a simple way to do remote procedure calls between applications, triggered on string pattern matching, transported by any way inside binary packets. It mainly defines packets encoding format to transmit data and pattern matching to select functions to call. Using network broadcast or multicast, it allows to send data in one shot to several systems at same time. Messages -------- Messages structure only contains: an **address string** (to be matched by patterns for routing to processing functions), **description of data** content (using type codes as defined in :ref:`typecodes`), and **data** itself (as packed binary). For basic OSC address pattern, see :ref:`address pattern`. Data in OSC messages are generally simple, like can be function parameters, some int or float numbers, string, boolean, an "impulse" or "bang" (OSC come from :abbr:`MIDI (Musical Instrument Digital Interface)` [#midi]_ musical world where a kind of “start” event is necessary). It could be used to transmit complex structures, but it's not its main purpose. See :ref:`typecodes` to see types allowed by :mod:`osc4py3`. .. [#midi] See https://en.wikipedia.org/wiki/MIDI Simple use ========== .. _example scripts: Examples -------- We will first start by small commented examples, then give some explanations and precisions. .. note:: Even if examples have been splitted in two parts, sending and receiving OSC messages can be realized in same program ; :func:`osc_startup`, :func:`osc_process` and :func:`osc_terminate` are client & server ready. Receiving OSC messages ^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python :linenos: # Import needed modules from osc4py3 from osc4py3.as_eventloop import * from osc4py3 import oscmethod as osm def handlerfunction(s, x, y): # Will receive message data unpacked in s, x, y pass def handlerfunction2(address, s, x, y): # Will receive message address, and message data flattened in s, x, y pass # Start the system. osc_startup() # Make server channels to receive packets. osc_udp_server("192.168.0.0", 3721, "aservername") osc_udp_server("0.0.0.0", 3724, "anotherserver") # Associate Python functions with message address patterns, using default # argument scheme OSCARG_DATAUNPACK. osc_method("/test/*", handlerfunction) # Too, but request the message address pattern before in argscheme osc_method("/test/*", handlerfunction2, argscheme=osm.OSCARG_ADDRESS + osm.OSCARG_DATAUNPACK) # Periodically call osc4py3 processing method in your event loop. finished = False while not finished: # … osc_process() # … # Properly close the system. osc_terminate() Sending OSC messages ^^^^^^^^^^^^^^^^^^^^ .. code-block:: python :linenos: # Import needed modules from osc4py3 from osc4py3.as_eventloop import * from osc4py3 import oscbuildparse # Start the system. osc_startup() # Make client channels to send packets. osc_udp_client("192.168.0.4", 2781, "aclientname") # Build a simple message and send it. msg = oscbuildparse.OSCMessage("/test/me", ",sif", ["text", 672, 8.871]) osc_send(msg, "aclientname") # Build a message with autodetection of data types, and send it. msg = oscbuildparse.OSCMessage("/test/me", None, ["text", 672, 8.871]) osc_send(msg, "aclientname") # Buils a complete bundle, and postpone its executions by 10 sec. exectime = time.time() + 10 # execute in 10 seconds msg1 = oscbuildparse.OSCMessage("/sound/levels", None, [1, 5, 3]) msg2 = oscbuildparse.OSCMessage("/sound/bits", None, [32]) msg3 = oscbuildparse.OSCMessage("/sound/freq", None, [42000]) bun = oscbuildparse.OSCBundle(oscbuildparse.unixtime2timetag(exectime), [msg1, msg2, msg3]) osc_send(bun, "aclientname") # Periodically call osc4py3 processing method in your event loop. finished = False while not finished: # You can send OSC messages from your event loop too… # … osc_process() # … # Properly close the system. osc_terminate() You can take a look at the :file:`rundemo.py` script in :file:`demos/` directory. It is a common demo for the different scheduling options, where main :mod:`osc4py3` functions are highlighted by surrounding comments. You can also look at :file:`speedudpsrv.py` and :file:`speedudpcli.py` in the :file:`demos/` directory. General layout ^^^^^^^^^^^^^^ So, the general layout for using :mod:`osc4py3` is 1. Import :mod:`osc4py3.as_xxx` module upon desired `threading model`_. 2. Import :mod:`osc4py3.oscbuildparse` if you plan to send messages. 3. Call :any:`osc_startup` function to start the system. 4. Create a set of client/server identified via channels names. 5. Register some handler functions with address matching. 6. Enter the processing loop. 1. Send created messages via client channels. 2. Call :any:`osc_process` function to run the system. 3. Received messages from server channels, have their address matching handlers called for you. 7. Call :any:`osc_terminate` to properly release resources (communications, threads…). Sending messages ---------------- As seen in :ref:`example scripts`, you need to import :mod:`oscbuildparse` module (or directly some of its content) to build a new :any:`OSCMessage` object. Then, simply and send it via :func:`osc_send` specifying target client channel names (see :ref:`client channels`). .. code-block:: python msg = oscbuildparse.OSCMessage("/test/me", ",sif", ["text", 672, 8.871]) osc_send(msg, "aclientname") You can see supported message data types in :ref:`typecodes`. It is also possible to send multiple OSC messages, or to request an immediate or on the contrary differed execution time using :any:`OSCBundle` (see :ref:`bundle`). .. code-block:: python bun = oscbuildparse.OSCBundle(unixtime2timetag(time.time()+30), [oscbuildparse.OSCMessage("/test/me", ",sif", ["none", -45, 11.34]), oscbuildparse.OSCMessage("/test/him", ",sif", ["two", 15, 11.856])]) osc_send(bun, "aclientname") bun = oscbuildparse.OSCBundle(.oscbuildparse.OSC_IMMEDIATELY, [oscbuildparse.OSCMessage("/test/me", ",sif", ["three", 92, 454]), oscbuildparse.OSCMessage("/test/him", ",sif", ["four", 107, -3e2])]) osc_send(bun, "aclientname") .. warning:: Note that when running the system with :mod:`osc4py3.as_eventloop`, several calls to :func:`osc_process` may be necessary to achieve complete processing of message sending (the message itself is generally sent / received in one :func:`osc_process` call if possible, but some monitoring status on sockets may need other calls to be stopped, and several sent messages may take time to be processed on sockets). Message handlers ---------------- A message handler is a Python callable (function or bound method, or `Python partial `_), which will be called when an :class:`OSCMessage`'s ``addresspattern`` is matched by a specified addrpattern filter. By default it is called with the (unpacked) OSC message data as parameters. You can build message handlers with an interface specifying each independent argument, like: .. code-block:: python def handler_set_location(x, y, z): # Wait for x,y,z of new location. Or use of ``*args`` variable arguments count. .. code-block:: python def handler_set_location(*args): # Wait for x,y,z of new location. .. hint:: If you want your method to be called even if sender gives an invalid parameters count, you may prefer the variable arguments count solution or the :const:`OSCARG_DATA` argscheme see below), else with an invalid argument count message, your handler call will fail immediately with a :class:`ValueError` exception. Installing message handlers ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The normal way is to use :func:`osc_method` function, where you associate your handler function with an OSC address pattern filter string expression (see :ref:`address pattern`). .. code-block:: python def osc_method(addrpattern, function [,argscheme[,extra]]) There is by default a simple mapping of the OSC message data to your function parameters, but you can change this using the ``argscheme`` parameter (see :ref:`argscheme`). You can provide an extra parameter(s) for your handler function, given as ``extra`` (you must require this parameter in your ``argscheme`` too). .. _address pattern: Address patterns ^^^^^^^^^^^^^^^^ When calling :any:`osc_method` function you specify the message address pattern filter as first parameter. .. code-block:: python def osc_method(addrpattern, function [,argscheme[,extra=None]]) By default the ``addrpattern`` follows OSC pattern model [#rewrite]_ , which looks like unix/dos globbing for filenames: - use ``/`` separators between "levels" - ``*`` match anything, operator stop at / boundary - ``//`` at beginning allow any level deep from start - ``?`` match any single char - ``[abcdef]`` match any any char in abcdef - ``[a-z]`` match any char from a to z - ``[!a-z]`` match any char non from a to z - ``{truc,machin,chose}`` match truc or machin or chose .. note:: There is no unbind function at this level of :mod:`osc4py3` use. .. [#rewrite] It is internally rewritten as Python :mod:`re` pattern to directly use Python regular expressions engine for matching. .. _argscheme: Arguments schemes ^^^^^^^^^^^^^^^^^ By default message data are unpacked as handler parameters. You can change what and how parameters are passed to handler's arguments with the ``argscheme`` parameter of :any:`osc_method` function. This parameter uses some tuple constant from :mod:`oscmethod` module, which can be added to build your handler function required arguments list. - :const:`OSCARG_DATA` — OSC message data tuple as one parameter. - :const:`OSCARG_DATAUNPACK` — OSC message data tuple as N parameters (default). - :const:`OSCARG_MESSAGE` — OSC message as one parameter. - :const:`OSCARG_MESSAGEUNPACK` — OSC message as three parameters (addrpattern, typetags, arguments). - :const:`OSCARG_ADDRESS` — OSC message address as one parameter. - :const:`OSCARG_TYPETAGS` — OSC message typetags as one parameter. - :const:`OSCARG_EXTRA` — Extra parameter you provide as one parameter - may be None. - :const:`OSCARG_EXTRAUNPACK` — Extra parameter you provide (unpackable) as N parameters. - :const:`OSCARG_METHODFILTER` — Method filter object as one parameter. This argument is a :class:`MethodFilter` internal structure containing informations about the message filter and your handler (argument scheme, filter expression…). - :const:`OSCARG_PACKOPT` — Packet options object as one parameter - may be None. This argument is a :class:`PacketOption` internal structure containing information about packet processing (source, time, etc). - :const:`OSCARG_READERNAME` — Name of transport channel which receive the OSC packet - may be None. - :const:`OSCARG_SRCIDENT` — Indentification information about packet source (ex. (ip,port)) - may be None. - :const:`OSCARG_READTIME` — Time when the packet was read - may be None. By combining these constants with **addition operator**, you can change **what arguments** your handler function will receive, and **in what order**. You can then adapt your handler function and/or the arguments scheme to fit. Examples:: # Import one of osc4py3 top level functions, and argument schemes definitions from osc4py3.as_allthreads import * # does osc_method from osc4py3.oscmethod import * # does OSCARG_XXX # Request the unpacked data (default). def fct(arg0, arg1, arg2, argn): pass def fct(*args): pass osc_method("/test/*", fct) # Request the message address pattern and the unpacked data. def fct(address, arg0, arg1, arg2, argn): pass def fct(address, *args): pass osc_method("/test/*", fct, argscheme=OSCARG_ADDRESS + OSCARG_DATAUNPACK) # Request the message address pattern and the packed data. def fct(address, args): pass osc_method("/test/*", fct, argscheme=OSCARG_ADDRESS + OSCARG_DATA) # Request the message address pattern, its type tag and the unppacked data. def fct(address, typetag, arg0, arg1, arg2, argn): pass def fct(address, typetag, *args): pass osc_method("/test/*", fct, argscheme=OSCARG_ADDRESS + OSCARG_TYPETAGS + OSCARG_DATAUNPACK) # Request the message address pattern, its type tag and the packed data… the OSCMessage parts. def fct(address, typetag, args): pass osc_method("/test/*", fct, argscheme=OSCARG_MESSAGEUNPACK) # Request the OSCMessage as a whole. def fct(msg): pass osc_method("/test/*", fct, argscheme=OSCARG_MESSAGE) # Request the message source identification, the whole message, and some extra data. def fct(srcident, msg, extra): pass osc_method("/test/*", fct, argscheme=OSCARG_SRCIDENT + OSCARG_MESSAGE + OSCARG_EXTRA, extra=dict(myoption1=4, myoption2="good")) .. hint:: Informations made available via argument scheme constants should be enough for large common usage. But, if some handler function need even more information from received messages, other members of internal objects coming with :const:`OSCARG_METHODFILTER` and :const:`OSCARG_PACKOPT` ``argscheme`` can be of interest. Threading model --------------- From :mod:`osc4py3` package, you must import functions from one of the :mod:`as_eventloop`, :mod:`as_comthreads` or :mod:`as_allthreads` modules (line 2 in examples). Each of these modules publish the same set of ``osc_…`` functions for different threading models (star import is safe with ``as_xxx`` modules, importing only the ``osc_fctxxx`` functions). All threads ^^^^^^^^^^^ Using :mod:`as_allthreads` module, threads are used in several places, including message handlers calls - by default a pool of ten working threads are allocated for message handlers calls. To control the final execution of methods, you can add an ``execthreadscount`` named parameter to :func:`osc_startup`. Setting it to 0 disable executions of methods in working threads, and all methods are called in the context of the raw packets processing thread, or if necessary in the context of the delayed bundle processing thread. Setting it to 1 build only one working thread to process methods calls, which are then all called in sequence in the context of that thread. If you want to protect your code against race conditions between your main thread and message handlers calls, you should better use :mod:`as_comthreads`. Communication threads ^^^^^^^^^^^^^^^^^^^^^ Using :mod:`as_comthreads` module, threads are used for communication operations (sending and receiving, intermediate processing), and messages are stored in queues between threads. Message handler methods are called only when you call yourself :func:`osc_process`. This is optimal if you want to achieve maximum background processing of messages transmissions, but final messages processing must occur within an event loop. No thread ^^^^^^^^^ Using :mod:`as_eventloop` module, there is no multi-threading, no background processing. All operations (communications and message handling) take place only when you call :func:`osc_process`. .. warning:: Multi-threading add some overhead in the whole processing (due to synchronization between threads). By example, on my computer, transmitting 1000 messages each executing a simple method call, :file:`speedudpsrv.py` goes from 0.2 sec with eventloop scheduling to 0.245 sec with comthreads scheduling and 0.334 sec with allthreads scheduling. But multi-threading can remain interesting if your main thread does C computing while osc4py3 process messages in background (which is the first use case of this development). .. important:: If you choose to run the system with :mod:`as_eventloop` or with :mod:`as_comthreads`, you **must** periodically call the function :func:`osc_process` sometime in your event loop. This call all code needed in the context of the event loop (all transmissions and dispatching code for :mod:`as_eventloop`, and only dispatching for :mod:`as_comthreads`). If you choose to run the system with :mod:`as_allthreads`, you don't need to call :func:`osc_process` (this function is defined in this module too, but does nothing). But you should have checked that your code will not have problems with concurrent access to resources (if needed there is a way to simply install a message handler to be called with an automatic Lock). Servers channels ---------------- Server channel are identified by names, but you should not have to use these names once the server channel has been created (unless you do advanced internal processing on incoming OSC packets). In the example server channels are simple UDP ports which will be listened to read some incoming datagram, first server listen for data from a specific IPV4 network, second server listen on all IPV4 networks. You can also create servers via :func:`osc_broadcast_server` or :func:`osc_multicast_server`. .. _client channels: Client channels --------------- Client channels are identified by names, allowing `light coupling`_ in your code when sending OSC messages. In the example client channels is a simple UDP transport. It will transmit data to server channels listening on the other side. You can also create clients via :func:`osc_broadcast_client` or :func:`osc_multicast_client` (providing ad-hoc broadcast/multicast addresses). Light coupling -------------- Client and server channels being identified via **names**, where name resolution is only done when communication activity is required, creation order of the different parts is not important. In this way, in your code you send a message to a named channel, without knowing how and where this channel connection has been established. You can change transport protocol with no impact on your communication code. If you target multiple channels via a list or tuple or set, eventually nested — this allow to easily define groups of channels as target for sending. If you use the special reserved ``"_local"`` name target, you send a message to be processed directly by your own program without network communication (no need to create a channel, it's a bypass name). Logging OSC operations ---------------------- Once imported, use of the package begin by initializing the system with a call to :func:`osc_startup` function. This function allows an optional ``logger`` named parameter, accepting a ``logging.logger`` object [#logging]_ . Example:: import logging logging.basicConfig(format='%(asctime)s - %(threadName)s ø %(name)s - ' '%(levelname)s - %(message)s') logger = logging.getLogger("osc") logger.setLevel(logging.DEBUG) osc_startup(logger=logger) .. [#logging] This is highly recommended in case of problems with multi-threads usage. Advanced pattern/handler ------------------------ You may setup your pattern / handler binding using low level functions, giving access to some more advanced options. .. code-block:: python from osc4py3 import oscmethod, oscdispatching mf = oscmethod.MethodFilter(...) oscdispatching.register_method(mf) By creating the :class:`osc4py3.oscmethod.MethodFilter` object directly, you can provide some extra construction parameters which are not available using high level :func:`osc_method` function (we don't list parameters which can be specified using ``argscheme``): - ``patternkind`` default to ``"osc"``, indicates to use OSC syntax for pattern. You can use here ``"re"`` to specify that you provide in ``addrpattern`` a Python regular expression. - ``workqueue`` allows you to specify a processing :class:`WorkQueue` associated to threads to call the handler with matched incoming messages — by default it's :const:`None` and indicate to use the common work queue. - ``lock`` can be used to specify a :class:`thread.Lock` (or :class:`thread.RLock`) object to acquire before processing matched messages with this handler function — by default it's :const:`None`. - ``logger`` is a specific logger to use to specifically trace that handler calling on matched messages reception. By default top level :func:`osc_method` functions use the logger given as :func:`osc_startup` ``logger`` parameter. User main functions =================== See :class:`OSCMessage` or :class:`OSCBundle` for their respective documentation. We document using the common :mod:`as_xxx` module .. note:: Startup and termination functions (:func:`osc_startup` and :func:`osc_terminate`) are intended to be called once only, at begining of program and at end of program. They install/uninstall a set of communication and filtering tools which normally stay active during program execution. Terminating then restarting osc4py3 may work… but has not been extensively tested. .. automodule:: osc4py3.as__common .. autofunction:: osc_startup .. autofunction:: osc_terminate .. autofunction:: osc_process .. autofunction:: osc_method .. autofunction:: osc_send .. autofunction:: osc_udp_server .. autofunction:: osc_udp_client .. autofunction:: osc_multicast_server .. autofunction:: osc_multicast_client .. autofunction:: osc_broadcast_server .. autofunction:: osc_broadcast_client