Package joy ::
Module 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
19 DEFAULT_PORT = 0xBAA
20
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
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
61 if not dic.has_key('type'):
62 return dic
63
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
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
80 if self.sock:
81 self.sock.close()
82 self.sock = None
83
85 """
86 (re)set the depth of the misc event queue
87 """
88 self.allow = allow
89 self._clearQueue()
90
92 """flush the misc event queue"""
93 self.queue = []
94
96 """
97 Set the socket binding address.
98
99 Will take effect next time the plan is started
100 """
101 self.bnd = bnd
102
104 if evt.type != TIMEREVENT:
105 return
106 for k in xrange(self.rate):
107 try:
108
109 pkt = self.sock.recv(1024)
110 dic = json_loads(pkt)
111
112 except SocketError,err:
113
114 if err.errno == EAGAIN:
115 break
116 raise
117
118 except ValueError,ve:
119
120
121 progress('Received bad UDP packet: %s' % repr(pkt))
122 continue
123
124
125
126 if dic.has_key('type'):
127 dic = self.convert(dic)
128
129 if not dic:
130 continue
131
132 if type(dic) is dict and dic.has_key('type'):
133
134 nev = JoyEvent( **dic )
135
136 postEvent(nev)
137 continue
138
139 if self.allow:
140 now = self.app.now
141 self._clearQueue()
142
143 self.queue.append( (now,dic) )
144 return False
145
147 """(private) clear old events from misc queue"""
148
149 now = self.app.now
150 while self.queue and (now-self.queue[0][0] > self.allow):
151 self.queue.pop(0)
152
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
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
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
192 self.sock = socket( AF_INET, SOCK_DGRAM )
193 self.sock.bind(('0.0.0.0',0))
194
196 if self.sock:
197 self.sock.close()
198 self.sock = None
199
201 """
202 Set the sink address. Will take effect immediately
203 """
204 self.dst = dst
205
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
222 if evt.type == TIMEREVENT:
223 return False
224 dic = describeEvt( evt, parseOnly = True )
225 self.sendMsg(**dic)
226 return False
227