source: trunk/puzzlebox_thinkgear_server_twisted.py @ 353

Last change on this file since 353 was 276, checked in by sc, 10 years ago

Puzzlebox Synapse:

  • split out into separate repository from Puzzlebox Brainstorms
File size: 11.8 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Puzzlebox - ThinkGear Emulator - Server
5#
6# Copyright Puzzlebox Productions, LLC (2010)
7#
8# This code is released under the GNU Pulic License (GPL) version 2
9# For more information please refer to http://www.gnu.org/copyleft/gpl.html
10#
11# Last Update: 2010.07.08
12#
13#####################################################################
14
15import math, os, signal, sys, time
16import simplejson as json
17
18from twisted.internet import reactor, protocol, defer, task
19from twisted.protocols import basic
20
21import puzzlebox_thinkgear_emulator_configuration as configuration
22#import puzzlebox_logger
23
24#####################################################################
25# Globals
26#####################################################################
27
28DEBUG = 1
29
30SERVER_INTERFACE = configuration.THINKGEAR_SERVER_INTERFACE
31SERVER_PORT = configuration.THINKGEAR_SERVER_PORT
32
33FLASH_POLICY_FILE_REQUEST = configuration.FLASH_POLICY_FILE_REQUEST
34FLASH_SOCKET_POLICY_FILE = configuration.FLASH_SOCKET_POLICY_FILE
35
36THINKGEAR_DELIMITER = '\r'
37
38MESSAGE_FREQUENCY_TIMER = 1 # 1 Hz
39
40ENABLE_SIMULATE_HEADSET_DATA = True
41
42BLINK_FREQUENCY_TIMER = 6 # blink every 6 seconds
43                          # (6 seconds is listed by Wikipedia
44                          # as being the average number of times
45                          # an adult blinks in a laboratory setting)
46
47DEFAULT_SAMPLE_WAVELENGTH = 10 # number of seconds from 0 to max to 0 for
48                               # any given detection value below
49
50DEFAULT_AUTHORIZATION_MESSAGE = \
51        {"isAuthorized": True}
52                # Tells the client whether the server has authorized
53                # access to the user's headset data. The value is
54                # either true or false.
55
56DEFAULT_SIGNAL_LEVEL_MESSAGE = \
57        {"poorSignalLevel": 0}
58                # A quantifier of the quality of the brainwave signal.
59                # This is an integer value that is generally in the
60                # range of 0 to 200, with 0 indicating a
61                # good signal and 200 indicating an off-head state.
62
63DEFAULT_EEG_POWER_MESSAGE = \
64        {"eegPower": { \
65                'delta': 0, \
66                'theta': 0, \
67                'lowAlpha': 0, \
68                'highAlpha': 0, \
69                'lowBeta': 0, \
70                'highBeta': 0, \
71                'lowGamma': 0, \
72                'highGamma': 0, \
73                }, \
74         } # A container for the EEG powers. These may
75           # be either integer or floating-point values.
76           # Maximum values are undocumented but assumed to be 65535
77
78DEFAULT_ESENSE_MESSAGE = \
79        {"eSense": { \
80                'attention': 0, \
81                'meditation': 0, \
82                }, \
83        } # A container for the eSense™ attributes.
84          # These are integer values between 0 and 100,
85          # where 0 is perceived as a lack of that attribute
86          # and 100 is an excess of that attribute.
87
88DEFAULT_BLINK_MESSAGE = {"blinkStrength": 255}
89        # The strength of a detected blink. This is
90        # an integer in the range of 0-255.
91
92DEFAULT_RAWEEG_MESSAGE = {"rawEeg": 255}
93        # The raw data reading off the forehead sensor.
94        # This may be either an integer or a floating-point value.
95
96DEFAULT_PACKET = {}
97DEFAULT_PACKET.update(DEFAULT_EEG_POWER_MESSAGE)
98DEFAULT_PACKET.update(DEFAULT_SIGNAL_LEVEL_MESSAGE)
99DEFAULT_PACKET.update(DEFAULT_ESENSE_MESSAGE)
100
101DEFAULT_RESPONSE_MESSAGE = DEFAULT_SIGNAL_LEVEL_MESSAGE
102
103#####################################################################
104# Classes
105#####################################################################
106
107class puzzlebox_thinkgear_server(protocol.ServerFactory):
108       
109        def __init__(self, log, DEBUG=DEBUG):
110               
111                self.log = log
112                self.DEBUG = DEBUG
113               
114                self.protocol = puzzlebox_thinkgear_server_protocol
115
116                self.message_frequency_timer = MESSAGE_FREQUENCY_TIMER
117                self.blink_frequency_timer = BLINK_FREQUENCY_TIMER
118
119                self.client_connected = False
120                self.connection_timestamp = time.time()
121                self.blink_timestamp = time.time()
122
123                self.packet_queue = []
124
125
126        ##################################################################
127
128        def validate_checksum(self, checksum):
129
130                '''The key used by the client application to identify
131itself. This must be 40 hexadecimal characters, ideally generated
132using an SHA-1 digest. The appKey is an identifier that is unique
133to each application, rather than each instance of an application.
134It is used by the server to bypass the authorization process if a
135user had previously authorized the requesting client. To reduce
136the chance of overlap with the appKey of other applications,
137the appKey should be generated using an SHA-1 digest.'''
138
139                is_valid = True
140
141                hexadecimal_characters = '0123456789abcdef'
142
143                if len(checksum) != 40:
144                        is_valid = False
145                else:
146                        for character in checksum:
147                                if character not in hexadecimal_characters:
148                                        is_valid = False
149               
150                return(is_valid)
151       
152       
153        ##################################################################
154       
155        def authorize_client(self, data):
156       
157                '''The client must initiate an authorization request
158and the server must authorize the client before the
159server will start transmitting any headset data.'''
160
161                is_authorized = self.validate_checksum(data['appKey'])
162
163                # A human-readable name identifying the client
164                # application. This can be a maximum of 255 characters.
165       
166                if len(data['appName']) > 255:
167                        is_authorized = False
168
169
170                return(is_authorized)
171       
172
173        ##################################################################
174
175        def calculate_wave_point(self, x, max_height=100, wave_length=10):
176
177                # start at 0, increase to max value at half of one
178                # wavelength, decrease to 0 by end of wavelength
179                y = ( (max_height/2) * \
180                      math.sin ((x-1) * ( math.pi / (wave_length / 2)))) + \
181                      (max_height/2)
182
183                # start at max value, decrease to 0 at half of one
184                # wavelegnth, increase to max by end of wavelength
185##              y = ( (max_height/2) * \
186##                    math.cos (x * ( math.pi / (wave_length / 2)))) + \
187##                    (max_height/2)
188
189
190                return(y)
191
192
193        ##################################################################
194
195        def simulate_headset_data(self):
196
197                response = DEFAULT_PACKET
198
199                time_value = self.connection_timestamp - time.time()
200
201                for key in response.keys():
202
203                        if key == 'poorSignalLevel':
204                                pass
205
206                        elif key == 'eSense':
207                                plot = self.calculate_wave_point( \
208                                        time_value, \
209                                        max_height=100, \
210                                        wave_length=DEFAULT_SAMPLE_WAVELENGTH)
211
212                                for each in response[key].keys():
213                                        response[key][each] = plot
214
215                        elif key == 'eegPower':
216                                plot = self.calculate_wave_point( \
217                                        time_value, \
218                                        max_height=65535, \
219                                        wave_length=DEFAULT_SAMPLE_WAVELENGTH)
220
221                                for each in response[key].keys():
222                                        response[key][each] = plot
223
224
225                return(response)
226
227
228        ##################################################################
229
230        def process_data(self, data):
231
232                d = defer.Deferred()
233
234                # Special socket handling for Flash applications
235                if (data == FLASH_POLICY_FILE_REQUEST):
236                        self.packet_queue.insert(0, FLASH_SOCKET_POLICY_FILE)
237                       
238                elif (type(data) == type({}) and \
239                      data.has_key('appName') and \
240                      data.has_key('appKey')):
241                        authorized = self.authorize_client(data)
242
243                        response = {}
244                        response['isAuthorized'] = authorized
245
246                        self.packet_queue.insert(0, response)
247
248                else:
249                        # No specific information needs to be returned
250                        pass
251
252               
253                if self.DEBUG:
254                        print "<-- [ThinkGear Emulator] Received:",
255                        print data
256
257               
258                d.callback(self.message_frequency_timer)
259
260               
261                return d
262       
263       
264        ##################################################################
265       
266        def process_connection_lost(self):
267
268                if self.DEBUG:
269                        print "--> [ThinkGear Emulator] Connection lost"
270
271                self.client_connected = False
272
273                try:
274                        self.looping_timer.stop()
275                except Exception, e:
276                        if self.DEBUG:
277                                print "ERROR: Failed to stop looping timer:",
278                                print e
279
280
281        ##################################################################
282
283##        def get_next_packet(self)
284##
285##                for packet in self.packet_queue:
286##
287##                        try:
288##                               
289##                                if packet.has_key('isAuthorized'):
290
291        ##################################################################
292
293        def update_status(self):
294
295                if ENABLE_SIMULATE_HEADSET_DATA:
296
297                        # Craft a simulated data packet
298                        self.packet_queue.append( \
299                                self.simulate_headset_data() )
300
301                        # Include simulated blinks at desired frequency
302                        if ((self.blink_frequency_timer != None) and \
303                            (time.time() - self.blink_timestamp > \
304                             self.blink_frequency_timer)):
305
306                                self.blink_timestamp = time.time()
307
308                                packet = DEFAULT_BLINK_MESSAGE
309                                self.packet_queue.append(packet)
310
311               
312        ##################################################################
313
314        def start_updating(self):
315
316                self.client_connected = True
317                self.connection_timestamp = time.time()
318                self.looping_timer = task.LoopingCall(self.update_status)
319                self.looping_timer.start(self.message_frequency_timer)
320
321
322#####################################################################
323# Protocol
324#####################################################################
325
326class puzzlebox_thinkgear_server_protocol(basic.LineReceiver):
327
328        delimiter='\r'
329       
330        def __init__(self):
331               
332                self.DEBUG = DEBUG
333                self.data_chunk = ""
334
335       
336        ##################################################################
337       
338        def connectionMade(self):
339               
340                if self.DEBUG:
341                        print "<---- [ThinkGear Emulator] Client connected"
342                       
343                self.factory.start_updating()
344
345
346        ##################################################################
347       
348        def noReply(self):
349               
350                try:
351                        self.factory.replyDefer.callback('NO_REPLY')
352                except:
353                        if self.DEBUG:
354                                print "noReply failed to call callback"
355               
356                self.transport.loseConnection()
357
358       
359        ##################################################################
360       
361        def dataReceived(self, data_received):
362
363                data_to_process = None
364               
365                self.data_chunk += data_received
366               
367                try:
368                        data_to_process = json.loads(self.data_chunk)
369               
370                except Exception, e:
371
372                        # Special socket handling for Flash applications
373                        if (data_received == FLASH_POLICY_FILE_REQUEST):
374                               
375                                if self.DEBUG:
376                                        print "<-- [ThinkGear Emulator] Flash policy file requested"
377                                       
378                                data_to_process = data_received
379                               
380                               
381                        else:
382                                if self.DEBUG:
383                                        print "<-- [ThinkGear Emulator] Partial data received (or error:",
384                                        print e
385                                        print ")."
386
387                                        print "data_chunk:",
388                                        print self.data_chunk
389
390
391                if (data_to_process != None):
392
393                        self.data_chunk = ""
394
395                        d = self.factory.process_data(data_to_process)
396                        d.addCallback(self.send_response)
397
398
399        ##################################################################
400       
401        def send_response(self, delay_timer):
402
403                while (len(self.factory.packet_queue) > 0):
404
405                        packet = self.factory.packet_queue[0]
406                        del self.factory.packet_queue[0]
407##                        packet = self.factory.packet_queue.pop()
408
409                        # Special socket handling for Flash applications
410                        if (packet != FLASH_SOCKET_POLICY_FILE):
411                                packet = json.dumps(packet)
412
413                        if self.DEBUG:
414                                print "--> [ThinkGear Emulator] Sending:",
415                                print packet
416
417                        self.sendLine(packet)
418
419
420                if self.factory.client_connected:
421
422                        reactor.callLater(delay_timer, \
423                                          self.send_response, delay_timer)
424
425                               
426        ##################################################################
427       
428        def connectionLost(self, reason):
429               
430                self.factory.process_connection_lost()
431
432
433#####################################################################
434# Main
435#####################################################################
436
437if __name__ == '__main__':
438       
439        #log = puzzlebox_logger.puzzlebox_logger(logfile='thinkgear_server')
440        log = None
441       
442        # Collect default settings and command line parameters
443        server_interface = SERVER_INTERFACE
444        server_port = SERVER_PORT
445       
446        for each in sys.argv:
447               
448                if each.startswith("--interface="):
449                        server_interface = each[ len("--interface="): ]
450                if each.startswith("--port="):
451                        server_port = each[ len("--port="): ]
452       
453       
454        thinkgear_server = puzzlebox_thinkgear_server(log, DEBUG=DEBUG)
455       
456        if DEBUG:
457                print "----> [ThinkGear Emulator] Initializing server on %s:%i" % \
458                        (server_interface, server_port)
459       
460        reactor.listenTCP(interface=server_interface, \
461                          port=server_port, \
462                          factory=thinkgear_server)
463        reactor.run()
464
Note: See TracBrowser for help on using the repository browser.