forked from esitarski/CrossMgr
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMultiCast.py
More file actions
294 lines (238 loc) · 7.67 KB
/
Copy pathMultiCast.py
File metadata and controls
294 lines (238 loc) · 7.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
import six
import socket
import struct
import sys
import time
import json
from six.moves.queue import Queue, Empty
from collections import deque
import threading
from datetime import datetime, timedelta
now = datetime.now
multicast_group = '225.3.14.15'
multicast_port = 10083
def ToJson( v ):
return json.dumps( v, separators=(',',':') )
def makeJSONCompatible( info ):
info = info.copy()
v = info['ts']
info['ts'] = [v.year, v.month, v.day, v.hour, v.minute, v.second, v.microsecond]
v = info['ts_start']
info['ts_start'] = [v.year, v.month, v.day, v.hour, v.minute, v.second, v.microsecond]
v = now()
info['ts_sender'] = [v.year, v.month, v.day, v.hour, v.minute, v.second, v.microsecond]
return info
class MultiCastSender( threading.Thread ):
'''
Thread to multicast messages written to an output queue.
Also inventories receivers.
'''
def __init__( self, qIn=None, receiverCallback=None, name='MultiCastSender' ):
super( MultiCastSender, self ).__init__()
self.name = name
self.daemon = True
self.hasReceivers = False
self.qIn = qIn or Queue()
self.receiverCallback = receiverCallback or (lambda receivers: None)
self.socket = None
def put( self, message, cmd='trigger' ):
self.qIn.put( (cmd, message) )
def getReceivers( self, sock ):
# Send our current time so the receivers can compute a clock correction.
receivers = []
tNow = now()
message = [
'idrequest',
{ 'ts_sender': [tNow.year, tNow.month, tNow.day, tNow.hour, tNow.minute, tNow.second, tNow.microsecond] }
]
try:
sent = sock.sendto(ToJson(message).encode(), (multicast_group, multicast_port))
except Exception as e:
return receivers
# Look for responses from all recipients
while 1:
try:
data, server = sock.recvfrom(4096)
except socket.timeout:
break
try:
response = json.loads( data.decode() )
except Exception as e:
continue
if response[0] == 'idreply':
response[1]['server'] = server
response[1]['ts_receiver'] = datetime( *response[1]['ts_receiver'] )
receivers.append( response[1] )
return receivers
def openSocket( self ):
# Create the datagram socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Set a timeout so the socket does not block indefinitely when trying
# to receive data.
sock.settimeout(0.3)
# Set the time-to-live for messages to 1 so they do not go past the
# local network segment.
ttl = struct.pack('b', 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
return sock
def run( self ):
timeoutSecs = 5.0
tLastReceiverQuery = now() - timedelta(seconds=timeoutSecs*2)
keepGoing = True
sock = None
while keepGoing:
message = None
try:
message = self.qIn.get( timeout=timeoutSecs )
except Empty:
pass
except TypeError:
break
if (now() - tLastReceiverQuery).total_seconds() >= timeoutSecs:
sock = self.openSocket()
receivers = self.getReceivers( sock )
sock.close()
sock = None
self.hasReceivers = bool( receivers )
self.receiverCallback( receivers )
tLastReceiverQuery = now()
if not message:
continue
# Clear all waiting messages.
messages = [message]
while 1:
try:
message = self.qIn.get( block=False )
messages.append( message )
except Empty:
break
# Broadcast all messages
sock = self.openSocket()
for message in messages:
if message[0] == 'trigger':
try:
sent = sock.sendto(ToJson([message[0], makeJSONCompatible(message[1])]).encode(), (multicast_group, multicast_port))
except Exception as e:
# six.print_( 'MultiCastSender:', e )
pass
elif message[0] == 'terminate':
keepGoing = False
break
sock.close()
sock = None
if sock:
sock.close()
qTrigger = None
sender = None
def SendTrigger( message ):
#
# message is a tuple: (cmd, d)
# d is an optional dict for the command.
#
# cmd can be:
#
# trigger - broadcast the trigger
# terminate - stop the sender thread.
#
# if cmd = trigger, dict must contain d['ts'] which is the timestamp of the trigger.
#
global qTrigger, sender
if not qTrigger:
qTrigger = Queue()
sender = MultiCastSender( qTrigger )
sender.start()
qTrigger.put( message )
def HasReceivers():
global sender
return sender and sender.hasReceivers()
#-----------------------------------------------------------------------
class MultiCastReceiver( threading.Thread ):
def __init__( self, triggerCallback, messageQ = None, name='MultiCastReceiver' ):
super( MultiCastReceiver, self ).__init__()
self.triggerCallback = triggerCallback
self.messageQ = messageQ
self.name = name
self.daemon = True
self.recentCorrections = deque( maxlen=32 )
def open( self ):
server_address = ('', multicast_port)
# Create the socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Bind to the server address
sock.bind(server_address)
# Tell the operating system to add the socket to the multicast group
# on all interfaces.
group = socket.inet_aton(multicast_group)
mreq = struct.pack('4sL', group, socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
return sock
def test( self ):
try:
self.open().close()
return None
except Exception as e:
return e
def run( self ):
now = datetime.now
sock = self.open()
while 1:
data, address = sock.recvfrom(4096)
tNow = now()
try:
message = json.loads( data.decode() )
except Exception as e:
continue
if message[0] == 'trigger':
message[1]['ts_receiver'] = tNow
# Convert the json values back to datetimes.
message[1]['ts'] = datetime( *message[1]['ts'] )
message[1]['ts_start'] = datetime( *message[1]['ts_start'] )
message[1]['ts_sender'] = datetime( *message[1]['ts_sender'] )
# Calculate a correction between the sender and the receiver. Add it to the median list.
self.recentCorrections.append( (message[1]['ts_receiver'] - message[1]['ts_sender']).total_seconds() )
# Return the median of the last corrections (a more robust estimate of the clock difference to ignore network disruption).
message[1]['correction_secs'] = sorted(self.recentCorrections)[len(self.recentCorrections)//2]
self.triggerCallback( message[1] )
elif message[0] == 'idrequest':
# Calculate a correction between the sender and the receiver. Add it to the median list.
self.recentCorrections.append( (tNow - datetime( *message[1]['ts_sender'] )).total_seconds() )
ts_receiver = [tNow.year, tNow.month, tNow.day, tNow.hour, tNow.minute, tNow.second, tNow.microsecond]
sock.sendto( ToJson(
['idreply', {
'hostname':socket.gethostname(),
'name':self.name,
'ts_receiver':ts_receiver,
'correction_secs': sorted(self.recentCorrections)[len(self.recentCorrections)//2],
}
]).encode(), address )
elif message[0] == 'terminate':
break
sock.close()
if __name__ == '__main__':
if len(sys.argv) == 2 and sys.argv[1].startswith('-r'):
# Receiver
triggerQ = Queue()
def triggerCallback( info ):
triggerQ.put( info )
def printQ():
while 1:
info = triggerQ.get()
six.print_( info )
triggerQ.task_done()
triggerPrinter = threading.Thread( target=printQ )
triggerPrinter.daemon = True
triggerPrinter.start()
receiver = MultiCastReceiver( triggerCallback )
receiver.start()
receiver.join()
else:
# Sender
def receiverCallback( receivers ):
six.print_( 'receivers:' )
for r in receivers:
six.print_( r )
for i in range(200):
SendTrigger( ('trigger', {'bib':200+i, 'team':'MyTeam', 'ts':now()}) )
time.sleep( 0.0001 if 10 < i < 20 else 1 )
qTrigger.put( ('terminate',) )
sender.join()