''' TUIO Input Provider =================== TUIO is the de facto standard network protocol for the transmission of touch and fiducial information between a server and a client. To learn more about TUIO (which is itself based on the OSC protocol), please refer to http://tuio.org -- The specification should be of special interest. Configure a TUIO provider in the config.ini ------------------------------------------- The TUIO provider can be configured in the configuration file in the ``[input]`` section:: [input] # name = tuio,: multitouchtable = tuio,192.168.0.1:3333 Configure a TUIO provider in the App ------------------------------------ You must add the provider before your application is run, like this:: from kivy.app import App from kivy.config import Config class TestApp(App): def build(self): Config.set('input', 'multitouchscreen1', 'tuio,0.0.0.0:3333') # You can also add a second TUIO listener # Config.set('input', 'source2', 'tuio,0.0.0.0:3334') # Then do the usual things # ... return ''' __all__ = ('TuioMotionEventProvider', 'Tuio2dCurMotionEvent', 'Tuio2dObjMotionEvent') from kivy.logger import Logger from functools import partial from collections import deque from kivy.input.provider import MotionEventProvider from kivy.input.factory import MotionEventFactory from kivy.input.motionevent import MotionEvent from kivy.input.shape import ShapeRect class TuioMotionEventProvider(MotionEventProvider): '''The TUIO provider listens to a socket and handles some of the incoming OSC messages: * /tuio/2Dcur * /tuio/2Dobj You can easily extend the provider to handle new TUIO paths like so:: # Create a class to handle the new TUIO type/path # Replace NEWPATH with the pathname you want to handle class TuioNEWPATHMotionEvent(MotionEvent): def depack(self, args): # In this method, implement 'unpacking' for the received # arguments. you basically translate from TUIO args to Kivy # MotionEvent variables. If all you receive are x and y # values, you can do it like this: if len(args) == 2: self.sx, self.sy = args self.profile = ('pos', ) self.sy = 1 - self.sy super().depack(args) # Register it with the TUIO MotionEvent provider. # You obviously need to replace the PATH placeholders appropriately. TuioMotionEventProvider.register('/tuio/PATH', TuioNEWPATHMotionEvent) .. note:: The class name is of no technical importance. Your class will be associated with the path that you pass to the ``register()`` function. To keep things simple, you should name your class after the path that it handles, though. ''' __handlers__ = {} def __init__(self, device, args): super().__init__(device, args) args = args.split(',') if len(args) == 0: Logger.error('Tuio: Invalid configuration for TUIO provider') Logger.error('Tuio: Format must be ip:port (eg. 127.0.0.1:3333)') err = 'Tuio: Current configuration is <%s>' % (str(','.join(args))) Logger.error(err) return ipport = args[0].split(':') if len(ipport) != 2: Logger.error('Tuio: Invalid configuration for TUIO provider') Logger.error('Tuio: Format must be ip:port (eg. 127.0.0.1:3333)') err = 'Tuio: Current configuration is <%s>' % (str(','.join(args))) Logger.error(err) return self.ip, self.port = args[0].split(':') self.port = int(self.port) self.handlers = {} self.oscid = None self.tuio_event_q = deque() self.touches = {} @staticmethod def register(oscpath, classname): '''Register a new path to handle in TUIO provider''' TuioMotionEventProvider.__handlers__[oscpath] = classname @staticmethod def unregister(oscpath, classname): '''Unregister a path to stop handling it in the TUIO provider''' if oscpath in TuioMotionEventProvider.__handlers__: del TuioMotionEventProvider.__handlers__[oscpath] @staticmethod def create(oscpath, **kwargs): '''Create a touch event from a TUIO path''' if oscpath not in TuioMotionEventProvider.__handlers__: raise Exception('Unknown %s touch path' % oscpath) return TuioMotionEventProvider.__handlers__[oscpath](**kwargs) def start(self): '''Start the TUIO provider''' try: from oscpy.server import OSCThreadServer except ImportError: Logger.info( 'Please install the oscpy python module to use the TUIO ' 'provider.' ) raise self.oscid = osc = OSCThreadServer() osc.listen(self.ip, self.port, default=True) for oscpath in TuioMotionEventProvider.__handlers__: self.touches[oscpath] = {} osc.bind(oscpath, partial(self._osc_tuio_cb, oscpath)) def stop(self): '''Stop the TUIO provider''' self.oscid.stop_all() def update(self, dispatch_fn): '''Update the TUIO provider (pop events from the queue)''' # read the Queue with event while True: try: value = self.tuio_event_q.pop() except IndexError: # queue is empty, we're done for now return self._update(dispatch_fn, value) def _osc_tuio_cb(self, oscpath, address, *args): self.tuio_event_q.appendleft([oscpath, address, args]) def _update(self, dispatch_fn, value): oscpath, command, args = value # verify commands if command not in [b'alive', b'set']: return # move or create a new touch if command == b'set': id = args[0] if id not in self.touches[oscpath]: # new touch touch = TuioMotionEventProvider.__handlers__[oscpath]( self.device, id, args[1:]) self.touches[oscpath][id] = touch dispatch_fn('begin', touch) else: # update a current touch touch = self.touches[oscpath][id] touch.move(args[1:]) dispatch_fn('update', touch) # alive event, check for deleted touch if command == b'alive': alives = args to_delete = [] for id in self.touches[oscpath]: if id not in alives: # touch up touch = self.touches[oscpath][id] if touch not in to_delete: to_delete.append(touch) for touch in to_delete: dispatch_fn('end', touch) del self.touches[oscpath][touch.id] class TuioMotionEvent(MotionEvent): '''Abstraction for TUIO touches/fiducials. Depending on the tracking software you use (e.g. Movid, CCV, etc.) and its TUIO implementation, the TuioMotionEvent object can support multiple profiles such as: * Fiducial ID: profile name 'markerid', attribute ``.fid`` * Position: profile name 'pos', attributes ``.x``, ``.y`` * Angle: profile name 'angle', attribute ``.a`` * Velocity vector: profile name 'mov', attributes ``.X``, ``.Y`` * Rotation velocity: profile name 'rot', attribute ``.A`` * Motion acceleration: profile name 'motacc', attribute ``.m`` * Rotation acceleration: profile name 'rotacc', attribute ``.r`` ''' __attrs__ = ('a', 'b', 'c', 'X', 'Y', 'Z', 'A', 'B', 'C', 'm', 'r') def __init__(self, *args, **kwargs): kwargs.setdefault('is_touch', True) kwargs.setdefault('type_id', 'touch') super().__init__(*args, **kwargs) # Default argument for TUIO touches self.a = 0.0 self.b = 0.0 self.c = 0.0 self.X = 0.0 self.Y = 0.0 self.Z = 0.0 self.A = 0.0 self.B = 0.0 self.C = 0.0 self.m = 0.0 self.r = 0.0 angle = property(lambda self: self.a) mot_accel = property(lambda self: self.m) rot_accel = property(lambda self: self.r) xmot = property(lambda self: self.X) ymot = property(lambda self: self.Y) zmot = property(lambda self: self.Z) class Tuio2dCurMotionEvent(TuioMotionEvent): '''A 2dCur TUIO touch.''' def depack(self, args): if len(args) < 5: self.sx, self.sy = list(map(float, args[0:2])) self.profile = ('pos', ) elif len(args) == 5: self.sx, self.sy, self.X, self.Y, self.m = list(map(float, args[0:5])) self.Y = -self.Y self.profile = ('pos', 'mov', 'motacc') else: self.sx, self.sy, self.X, self.Y = list(map(float, args[0:4])) self.m, width, height = list(map(float, args[4:7])) self.Y = -self.Y self.profile = ('pos', 'mov', 'motacc', 'shape') if self.shape is None: self.shape = ShapeRect() self.shape.width = width self.shape.height = height self.sy = 1 - self.sy super().depack(args) class Tuio2dObjMotionEvent(TuioMotionEvent): '''A 2dObj TUIO object. ''' def depack(self, args): if len(args) < 5: self.sx, self.sy = args[0:2] self.profile = ('pos', ) elif len(args) == 9: self.fid, self.sx, self.sy, self.a, self.X, self.Y = args[:6] self.A, self.m, self.r = args[6:9] self.Y = -self.Y self.profile = ('markerid', 'pos', 'angle', 'mov', 'rot', 'motacc', 'rotacc') else: self.fid, self.sx, self.sy, self.a, self.X, self.Y = args[:6] self.A, self.m, self.r, width, height = args[6:11] self.Y = -self.Y self.profile = ('markerid', 'pos', 'angle', 'mov', 'rot', 'rotacc', 'acc', 'shape') if self.shape is None: self.shape = ShapeRect() self.shape.width = width self.shape.height = height self.sy = 1 - self.sy super().depack(args) class Tuio2dBlbMotionEvent(TuioMotionEvent): '''A 2dBlb TUIO object. # FIXME 3d shape are not supported /tuio/2Dobj set s i x y a X Y A m r /tuio/2Dblb set s x y a w h f X Y A m r ''' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.profile = ('pos', 'angle', 'mov', 'rot', 'rotacc', 'acc', 'shape') def depack(self, args): self.sx, self.sy, self.a, self.X, self.Y, sw, sh, sd, \ self.A, self.m, self.r = args self.Y = -self.Y if self.shape is None: self.shape = ShapeRect() self.shape.width = sw self.shape.height = sh self.sy = 1 - self.sy super().depack(args) # registers TuioMotionEventProvider.register(b'/tuio/2Dcur', Tuio2dCurMotionEvent) TuioMotionEventProvider.register(b'/tuio/2Dobj', Tuio2dObjMotionEvent) TuioMotionEventProvider.register(b'/tuio/2Dblb', Tuio2dBlbMotionEvent) MotionEventFactory.register('tuio', TuioMotionEventProvider)