python-osc icon indicating copy to clipboard operation
python-osc copied to clipboard

problem with server and client

Open mattfack opened this issue 4 years ago • 24 comments

I am a fairly experienced python user, but I have no experience with OSC protocol.

I am helping a friend in building a python code that should do the following:

  1. receive a message from MAX via OSC
  2. elaborate the message in python
  3. send the elaborated message to SuperCollider via OSC

What is not clear to me is what concerns points 1 and 3

PART 0 import methods

from pythonosc import dispatcher
from pythonosc import osc_server
from pythonosc import udp_client

PART 1 setup a simple server for point 1, since I am only receiving one string at a time from MAX:

def listen2Max(ip,port):
    '''
    set up server
    '''
    # dispatcher to receive message
    dispatcher = dispatcher.Dispatcher()
    dispatcher.map("/filter", print)
    # server to listen
    server = osc_server.ThreadingOSCUDPServer((ip,port), dispatcher)
    print("Serving on {}".format(server.server_address))
    server.serve_forever()

but i do not know what to return in this function, also I do not get the use of "/filter" in the map. Is it something that must be specified in MAX?

PART 2

elaborate message with python

some python code that returns mymove

PART 3

set up a simple client to communicate the string var mymove to SuperCollider

def talk2SC(ip,port,mymove):
    '''
    set up client
    '''
    client = udp_client.SimpleUDPClient(ip,port)
    client.send_message("/filter", mymove)

Should it work like that?

mattfack avatar Feb 08 '21 17:02 mattfack

  1. /filter is just an example, MAX is going to send OSC packets to a URI, that's the path part of it, I don't know how MAX works but you should be able to customise that URI in MAX.
  2. looks fine :)
  3. avoid setting-up a new client each time you want to send/forward a message it's potentially expensive/time-consuming to do so.

What you want is basically an OSC "proxy", so you need to set up listening for max input first, set up your forwarding client next, then plug the twos together.

I recommend reading the OSC spec (https://zenodo.org/record/1177517) to familiarize yourself with the concepts.

attwad avatar Feb 09 '21 13:02 attwad

Cool, thanks. I will read the document :)

As to 1: usually I have something like

myvar = somefunction(variables)

What is this part with the client?

mattfack avatar Feb 11 '21 07:02 mattfack

Glad this was helpful. This library doesn't impose any special structure on your code, you can have a single class that does the server listening and client forwarding in that case the function you mentioned would live in that class as well, or it can be a standalone function, up to you really.

attwad avatar Feb 11 '21 07:02 attwad

Thanks! I do not want to take advantage of your helpfulness but if I was to use a single function as described here


def listen2Max(ip,port):
    '''
    set up server
    '''
    # dispatcher to receive message
    dispatcher = dispatcher.Dispatcher()
    dispatcher.map("/filter", print)
    # server to listen
    server = osc_server.ThreadingOSCUDPServer((ip,port), dispatcher)
    print("Serving on {}".format(server.server_address))
    server.serve_forever()

What would be the variable "containing" the message? Something like server.items() or...?

mattfack avatar Feb 11 '21 07:02 mattfack

The print function that you map to the dispatcher is a function that will be called with the osc params from MAX as arguments each time a new message comes in.

attwad avatar Feb 11 '21 08:02 attwad

Ok, got it and the server part works like a charm!

Now, any suggestions on how to plug the server and client? I suppose, I should not use the same port.

mattfack avatar Feb 11 '21 11:02 mattfack

You can pick whatever you want for your client port, best to make it configurable by users using an environment variable or a flag but that's up to you.

attwad avatar Feb 11 '21 11:02 attwad

Great! I did it ;)

You are my hero now, know it :)

is there a way to ship the ip address along with the function call in dispatcher.map("/filter", myfunc)?

something like dispatcher.map("/filter", myfunc(myip)) does not work.

I tried to rescue the ip from within myfunc which is defined as

def myfunc(path: str, *osc_arguments):

with no luck

Also, I did not get how to pass arguments to myfunc such as, e.g., the custom output port

mattfack avatar Feb 11 '21 11:02 mattfack

Sure thing, dispatcher.map accepts an optional list of arguments at the end to pass static data like that: https://github.com/attwad/python-osc/blob/master/pythonosc/dispatcher.py#L70

So it should be: dispatcher.map("/filter", myfunc, myip)

attwad avatar Feb 11 '21 11:02 attwad

Ok, cool, got it! Now we are trying to find out Max's URI because we are not able to make max communicate with python on the same ip/port

mattfack avatar Feb 11 '21 15:02 mattfack

It seems like max has no URI path, or at least not one that I could find.

Is there a way to make my server listen to each path on that port?

mattfack avatar Feb 13 '21 10:02 mattfack

You could use a default handler: https://github.com/attwad/python-osc/blob/master/pythonosc/dispatcher.py#L198

attwad avatar Feb 13 '21 15:02 attwad

Ok, so at the moment I am passing my main function to the dispatcher like this:

disp = dispatcher.Dispatcher()
disp.map(pathIN, main, ipIN, portOUT, pathOUT)

inside the main all the magic happens, where does the default handler will be used?

I see it requires a function as argument, which one? The main?

EDIT: I think I know what you mean. The default handler could be used to get max's address right?

mattfack avatar Feb 14 '21 22:02 mattfack

Well, I tried to

  1. use set_default_handler instead of map with no luck because it changes the port every time it gets a message, thus it is impossible to read outgoing messages from another server
  2. use set_default_handler to return the default address and pass it to map

Obviously, I am missing something.

Here is my python code, I hope someone could help me understand what I am doing wrong.

import argparse

from pythonosc import dispatcher
from pythonosc import osc_server
from pythonosc import udp_client

def main(path: str, *osc_arguments):
    msg = osc_arguments[-1]
    print("input message: {}".format(msg))
    msgOUT = msg+'out'
    # output
    print("output message: {}".format(msgOUT))
    ipOUT = osc_arguments[0][0]
    portOUT = osc_arguments[0][1]
    pathOUT= osc_arguments[0][2]
    talk2SC(ipOUT,portOUT,pathOUT,msgOUT)

def listen2Max(addrIN,addrOUT):
    '''
    set up server
    '''
    # input address
    ipIN   = addrIN[0]
    portIN = addrIN[1]
    pathIN = addrIN[2]
    # output address
    portOUT = addrOUT[0]
    pathOUT = addrOUT[1]
    # dispatcher to receive message
    disp = dispatcher.Dispatcher()
    disp.map(pathIN, main, ipIN, portOUT, pathOUT)
    # server to listen
    server = osc_server.ThreadingOSCUDPServer((ipIN,portIN), disp)
    print("Serving on {}".format(server.server_address))
    server.serve_forever()

def talk2SC(ip,port,path,mymove):
    '''
    set up client
    '''
    client = udp_client.SimpleUDPClient(ip,port)
    client.send_message(path, mymove)

if __name__ == "__main__":
    # generate parser
    parser = argparse.ArgumentParser(prog='scacchiOSC', formatter_class=argparse.RawDescriptionHelpFormatter, description='Interprete di messaggi OSC da Max\n')
    parser.add_argument("-II","--ipIN", type=str, default="127.0.0.1", help="The ip to listen on")
    parser.add_argument("-PI", "--portIN", type=int, default=5005, help="The port to listen on")
    parser.add_argument("-UI", "--uripathIN", type=str, default="/filter", help="MAX's URI path")
    parser.add_argument("-PO", "--portOUT", type=int, default=5006, help="The port to send messages to")
    parser.add_argument("-UO", "--uripathOUT", type=str, default="/filter", help="output URI path")
    args = parser.parse_args()
    # wrap up inputs
    outputAddress = [args.portOUT, args.uripathOUT]
    inputAddress = [args.ipIN, args.portIN, args.uripathIN]
    # listen to max
    listen2Max(inputAddress, outputAddress)

mattfack avatar Feb 15 '21 09:02 mattfack

I solved one of my problems:

on max's side, string messages must be formatted as follows:

/filter "my message with spaces"

where filter is the URI path one chooses.

If one does not want to have a fixed URI path one could choose to format messages on max like this:

/ "my message with spaces"

and in python one would simply write in the parser:

parser.add_argument("-UI", "--uripathIN", type=str, default="*", help="MAX's URI path")

Now, I need to figure out on which path supercollider will listen to python!

mattfack avatar Feb 15 '21 18:02 mattfack

I ended up sending and receiving messages with "pure" udp protocol to skip the address part

Now I am fighting with OSC byte-type.

Is there a function to

  1. translate a OSC object to string even if they are coming through a socket?
  2. translate a string to OSC object to send it through a socket?

mattfack avatar Feb 16 '21 15:02 mattfack

Sorry I'm not sure I follow anymore, if you end up dealing with OSC from raw UDP packets then you basically bypass what this library is about so I'm not sure how useful this is to you.

attwad avatar Feb 16 '21 20:02 attwad

I am using some of the functions of this library that are helping me in translating strings to osc objects and osc objects to strings!

mattfack avatar Feb 16 '21 21:02 mattfack

Ah ok, well you can look at the functions in https://github.com/attwad/python-osc/blob/master/pythonosc/parsing/osc_types.py#L49 for example but this is not really a use case supported by this library so you might not find exactly what you want here.

attwad avatar Feb 17 '21 09:02 attwad

Yes, I have seen it and used it.

For the purpose of this open issue, I would "make a request": allow for raw UDP connection WITH OSC object transmission.

This is what I have roughly done using a little bit of this library and a little bit of the socket library. For the infrastructure of this library, it shouldn't be too hard to add a server and a client type for raw udp connections...of course if it is in the scope of your work!

mattfack avatar Feb 17 '21 09:02 mattfack

FYI the existing client is a UDP client and there exists UDP servers as well: https://github.com/attwad/python-osc/blob/master/pythonosc/udp_client.py https://github.com/attwad/python-osc/blob/master/pythonosc/osc_server.py#L18

I am still unclear why you need to work on raw packets but if you think this is legitimate and could be useful to others then PRs are welcome :)

attwad avatar Feb 19 '21 15:02 attwad

Messages coming from MaxMSP are raw packets. Plus, my friend wasn't sure about how to set the address in Super Collider. Of course, everything is feasible both in MaxMSP and SuperCollider, but still I think it could be a good feature to make the address optional...but of course it is up to you ;)

mattfack avatar Feb 20 '21 13:02 mattfack

Yes messages transit "raw" over UDP sockets, that's why this library exists, to parse them. I'd prefer if folks understand how their clients works before adding custom code in this library as a workaround. I'm still unclear about your comment on https://github.com/attwad/python-osc/issues/129#issuecomment-779070339 as to why the default handler doesn't work for you, it's meant to be used when you don't care about the address which is what you seem to want. I'm sorry I don't use max or supercollider so I can't really test locally perhaps I'm missing something but I expect from any reasonable OSC client or server to have some settings that fix the port and provide address mapping to various messages they send. Both Max and supercollider are quite major products so this is why I'm a bit confused here.

Perhaps those can help:

  • https://www.ableton.com/en/packs/connection-kit/ (port and addresses seem to be mappable quite easily here image )
  • https://doc.sccode.org/Guides/OSC_communication.html (seems to indicate SC will always listen on port 57120 by default)

attwad avatar Feb 20 '21 19:02 attwad

Thanks, I admit I might have misunderstood the use of the default handler. Would you mind providing a simple piece of code to help me understand its use?

mattfack avatar Feb 22 '21 09:02 mattfack