Package joy :: Module remote
[hide private]
[frames] | no frames]

Source Code for Module joy.remote

  1   
  2   
  3  """ 
  4  File joy.remote contains the remote.Source and remote.Sink Plan  
  5  subclasses. These Plan-s may be used to relay remote JoyApp events  
  6  between processes that may even be running on different machines 
  7   
  8  """ 
  9   
 10  from plans import Plan 
 11  from joy.events import JoyEvent, describeEvt 
 12  from pygix import TIMEREVENT, KEYUP, KEYDOWN, JOYBUTTONUP, JOYBUTTONDOWN, postEvent 
 13  from socket import socket, AF_INET, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR, error as SocketError 
 14  from errno import EAGAIN 
 15  from json import loads as json_loads, dumps as json_dumps 
 16  from loggit import progress 
 17   
 18  # Default UDP port used for communications 
 19  DEFAULT_PORT = 0xBAA 
 20   
21 -class Sink( Plan ):
22 """ 23 Concrete class remote.Sink 24 25 This Plan subclass may be used to relay via UDP JoyApp events (including all pygame events) from a remote.Source running in 26 another JoyApp, potentially on another machine. 27 28 Events are serialized into a JSON string and sent over separate UDP 29 packets. TIMEREVENTS are ignored. 30 31 Additionally, any JSON packets that contain dictionaries without 32 the 'type' key may be shunted to a separate queue, for application 33 specific processing. This queue is windowed in time, i.e. will hold 34 at most X seconds worth of packets, as specified by the allowMisc 35 constructor parameter. Its contents are accessible via queueIter(). 36 """ 37 DEFAULT_BINDING = ('0.0.0.0',DEFAULT_PORT) 38
39 - def __init__( self, app, bnd=DEFAULT_BINDING, rate=100, convert=None, allowMisc = None ):
40 """ 41 Attributes: 42 bnd -- 2-tuple -- binding address for socket, in socket library format 43 rate -- integer -- maximal number of events processed each time-slice 44 sock -- socket / None -- socket only exists while plan is running 45 convert -- callable -- convert incoming event dictionaries. 46 Allows remote joysticks/keys to be remapped. Returns None if 47 event should be ignored 48 By default, only KEYUP and KEYDOWN events are allowed 49 allowMisc -- float / None -- number of seconds of "misc" packets 50 to store in self.queue or None to disallow non-event packets 51 """ 52 Plan.__init__(self,app) 53 self.bnd = bnd 54 self.sock = None 55 self.rate = rate 56 self.flushMisc() 57 self.setAllowMisc(allowMisc) 58 if convert is None: 59 def default_convert( dic ): 60 # Non-event messages --> passthrough 61 if not dic.has_key('type'): 62 return dic 63 # Filter events by type_code 64 tc = dic['type_code'] 65 if tc in {KEYUP,KEYDOWN,JOYBUTTONUP,JOYBUTTONDOWN} : 66 return dic 67 return None
68 self.convert = default_convert 69 else: 70 assert callable(convert) 71 self.convert = convert
72
73 - def onStart( self ):
74 self.sock = socket( AF_INET, SOCK_DGRAM ) 75 self.sock.setsockopt( SOL_SOCKET, SO_REUSEADDR, 1 ) 76 self.sock.bind(self.bnd) 77 self.sock.setblocking(False)
78
79 - def onStop( self ):
80 if self.sock: 81 self.sock.close() 82 self.sock = None
83
84 - def setAllowMisc( self, allow ):
85 """ 86 (re)set the depth of the misc event queue 87 """ 88 self.allow = allow 89 self._clearQueue()
90
91 - def flushMisc( self ):
92 """flush the misc event queue""" 93 self.queue = []
94
95 - def setSink( self, bnd ):
96 """ 97 Set the socket binding address. 98 99 Will take effect next time the plan is started 100 """ 101 self.bnd = bnd
102
103 - def onEvent( self, evt ):
104 if evt.type != TIMEREVENT: 105 return 106 for k in xrange(self.rate): 107 try: 108 # Create event from the next packet 109 pkt = self.sock.recv(1024) 110 dic = json_loads(pkt) 111 # 112 except SocketError,err: 113 # If socket is out of data --> we're done 114 if err.errno == EAGAIN: 115 break 116 raise 117 # 118 except ValueError,ve: 119 # Value errors come from JSON decoding problems. 120 # --> Log and continue 121 progress('Received bad UDP packet: %s' % repr(pkt)) 122 continue 123 # 124 # Process the event packet 125 # 126 if dic.has_key('type'): 127 dic = self.convert(dic) 128 # If converter dropped event --> next 129 if not dic: 130 continue 131 # If has a 'type' --> JoyEvent 132 if type(dic) is dict and dic.has_key('type'): 133 # Put event on event queue 134 nev = JoyEvent( **dic ) 135 #DBG progress('Remote event:'+str(nev)) 136 postEvent(nev) 137 continue 138 # If custom events are allowed --> add to queue 139 if self.allow: 140 now = self.app.now 141 self._clearQueue() 142 # Store new timestamp and packet 143 self.queue.append( (now,dic) ) 144 return False
145
146 - def _clearQueue(self):
147 """(private) clear old events from misc queue""" 148 # Drop any out-of-date packets from queue 149 now = self.app.now 150 while self.queue and (now-self.queue[0][0] > self.allow): 151 self.queue.pop(0)
152
153 - def queueIter( self ):
154 """Iterate over the custom packets in the queue 155 Yields pairs (ts, pkt) 156 ts -- float -- arrival time 157 pkt -- dictionary -- packet contents 158 """ 159 if self.allow is None: 160 raise IndexError,"No custom packets allowed -- use allowMisc parameter to Sink constructor" 161 while self.queue: 162 if (self.app.now-self.queue[0][0]) < self.allow: 163 yield self.queue[0] 164 self.queue.pop(0)
165
166 -class Source( Plan ):
167 """ 168 Concrete class remote.Source 169 170 This Plan subclass may be used to relay JoyApp events (including all 171 pygame events) to a remote.Sink running in another JoyApp via UDP. 172 This allows a controller in one JoyApp to control a client in a 173 remote JoyApp connected via an IP network, potentially running on 174 another host altogether. 175 176 Events are serialized into a JSON string and sent over separate UDP 177 packets. TIMEREVENTS are ignored. 178 """ 179 DEFAULT_SINK = ('localhost',DEFAULT_PORT) 180
181 - def __init__( self, app, dst=DEFAULT_SINK ):
182 """ 183 Attributes: 184 dst -- 2-tuple -- destination address for packets, in socket library format 185 sock -- socket / None -- socket only exists while plan is running 186 """ 187 Plan.__init__(self,app) 188 self.dst = dst 189 self.sock = None
190
191 - def onStart( self ):
192 self.sock = socket( AF_INET, SOCK_DGRAM ) 193 self.sock.bind(('0.0.0.0',0))
194
195 - def onStop( self ):
196 if self.sock: 197 self.sock.close() 198 self.sock = None
199
200 - def setSink( self, dst ):
201 """ 202 Set the sink address. Will take effect immediately 203 """ 204 self.dst = dst
205
206 - def sendMsg( self, **msg ):
207 """ 208 Send a dictionary to the remote sink; will appear in its 209 misc message queue unless it has the key "type", in which 210 case the sink will try to convert it to a JoyApp event. 211 212 WARNING: if you use "type" inappropriately, the sink 213 will error out and stop running 214 """ 215 assert not msg.has_key("t"), "Reserved for sender timestamp" 216 msg['t'] = self.app.now 217 jsn = json_dumps(msg, ensure_ascii = True ) 218 self.sock.sendto( jsn, self.dst ) 219 progress( "Sending '%s' to %s:%d" % ((jsn,)+self.dst) )
220
221 - def onEvent( self, evt ):
222 if evt.type == TIMEREVENT: 223 return False 224 dic = describeEvt( evt, parseOnly = True ) 225 self.sendMsg(**dic) 226 return False
227