source: thinkgear_emulator/puzzlebox_thinkgear_serial_protocol.py @ 122

Last change on this file since 122 was 122, checked in by sc, 11 years ago

thinkgear_emulator/puzzlebox_thinkgear_serial_protocol.py:

  • data payload parsing complete
  • Property svn:executable set to *
File size: 17.9 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Puzzlebox - Brainstorms - Network - Server - ThinkGear - Serial Protocol
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.22
12#
13#####################################################################
14# SPEC:
15#
16# CODE Definitions Table
17# Single-Byte CODEs
18# Extended             (Byte)
19# Code Level   [CODE] [LENGTH] Data Value Meaning
20# ----------   ------ -------- ------------------
21#           0    0x02        - POOR_SIGNAL Quality (0-255)
22#           0    0x04        - ATTENTION eSense (0 to 100)
23#           0    0x05        - MEDITATION eSense (0 to 100)
24#           0    0x16        - Blink Strength. (0-255) Sent only
25#                              when Blink event occurs.
26# Multi-Byte CODEs
27# Extended             (Byte)
28# Code Level   [CODE] [LENGTH] Data Value Meaning
29# ----------   ------ -------- ------------------
30#           0    0x80        2 RAW Wave Value: a single big-endian
31#                                16-bit two's-compliment signed value
32#                                (high-order byte followed by
33#                                low-order byte) (-32768 to 32767)
34#           0    0x83       24 ASIC_EEG_POWER: eight big-endian
35#                                3-byte unsigned integer values
36#                                representing delta, theta, low-alpha
37#                                high-alpha, low-beta, high-beta,
38#                                low-gamma, and mid-gamma EEG band
39#                                power values
40#         Any    0x55        - NEVER USED (reserved for [EXCODE])
41#         Any    0xAA        - NEVER USED (reserved for [SYNC])
42#####################################################################
43# Linux Bluetooth serial protocol profile example:
44#    rfcomm connect rfcomm0 00:13:EF:00:1B:FE 3
45#####################################################################
46# TODO:
47# - needs to handle:
48#   serial.serialutil.SerialException:
49#   could not open port /dev/rfcomm0:
50#   [Errno 16] Device or resource busy: '/dev/rfcomm0'
51#####################################################################
52
53import sys
54import serial
55
56#from PyQt4 import QtCore, QtNetwork
57
58import puzzlebox_thinkgear_emulator_configuration as configuration
59#import puzzlebox_logger
60
61#####################################################################
62# Globals
63#####################################################################
64
65DEBUG = 2
66
67DEFAULT_SERIAL_PORT_WINDOWS = 'COM2'
68DEFAULT_SERIAL_PORT_LINUX = '/dev/rfcomm0'
69
70if (sys.platform == 'win32'):
71        DEFAULT_SERIAL_PORT = DEFAULT_SERIAL_PORT_WINDOWS
72else:
73        DEFAULT_SERIAL_PORT = DEFAULT_SERIAL_PORT_LINUX
74
75DEFAULT_SERIAL_BAUDRATE = 57600
76
77DEFAULT_MINDSET_ADDRESS = '00:13:EF:00:1B:FE'
78
79PROTOCOL_SYNC = 'xAA'
80PROTOCOL_EXCODE = '\x55'
81
82DEBUG_BYTE_COUNT = 8192
83DEBUG_PACKET_COUNT = 12
84
85#####################################################################
86# Classes
87#####################################################################
88
89class puzzlebox_thinkgear_serial_protocol:
90       
91        def __init__(self, log, \
92                               serial_port=DEFAULT_SERIAL_PORT, \
93                               DEBUG=DEBUG, \
94                               parent=None):
95               
96                self.log = log
97                self.DEBUG = DEBUG
98               
99                self.serial_port = serial_port
100                self.device = None
101                self.buffer = ''
102               
103                self.device = self.initialize_device()
104       
105       
106        ##################################################################
107       
108        def initialize_device(self):
109               
110                baudrate = DEFAULT_SERIAL_BAUDRATE
111                bytesize = 8
112                parity = 'NONE'
113                stopbits = 1
114                software_flow_control = 'f'
115                rts_cts_flow_control = 'f'
116                #timeout = 15
117                timeout = 5
118               
119                # convert bytesize
120                if (bytesize == 5):
121                        init_byte_size = serial.FIVEBITS
122                elif (bytesize == 6):
123                        init_byte_size = serial.SIXBITS
124                elif (bytesize == 7):
125                        init_byte_size = serial.SEVENBITS
126                elif (bytesize == 8):
127                        init_byte_size = serial.EIGHTBITS
128                else:
129                        #self.log.perror("Invalid value for %s modem byte size! Using default (8)" % modem_type)
130                        init_byte_size = serial.EIGHTBITS
131               
132                # convert parity
133                if (parity == 'NONE'):
134                        init_parity = serial.PARITY_NONE
135                elif (parity == 'EVEN'):
136                        init_parity = serial.PARITY_EVEN
137                elif (parity == 'ODD'):
138                        init_parity = serial.PARITY_ODD
139                else:
140                        #self.log.perror("Invalid value for %s modem parity! Using default (NONE)" % modem_type)
141                        init_parity = serial.PARITY_NONE
142               
143                # convert stopbits
144                if (stopbits == 1):
145                        init_stopbits = serial.STOPBITS_ONE
146                elif (stopbits == 2):
147                        init_stopbits = serial.STOPBITS_TWO
148                else:
149                        #self.log.perror("Invalid value for %s modem stopbits! Using default (8)" % modem_type)
150                        init_byte_size = serial.STOPBITS_ONE
151               
152                # convert software flow control
153                if (software_flow_control == 't'):
154                        init_software_flow_control = 1
155                else:
156                        init_software_flow_control = 0
157               
158                # convert rts cts flow control
159                if (rts_cts_flow_control == 't'):
160                        init_rts_cts_flow_control = 1
161                else:
162                        init_rts_cts_flow_control = 0
163               
164               
165                # Initialize the modem
166                #self.log.pdebug("Initializing %s modem" % modem_code)
167               
168                device = serial.Serial(port = serial_port, \
169                                            baudrate = baudrate, \
170                                            bytesize = init_byte_size, \
171                                            parity = init_parity, \
172                                            stopbits = init_stopbits, \
173                                            xonxoff = init_software_flow_control, \
174                                            rtscts = init_rts_cts_flow_control, \
175                                            timeout = timeout)
176               
177               
178                return(device)
179       
180       
181        ##################################################################
182       
183        def communicate_with_handsfree_profile(self):
184               
185                #"AT+CKPD=200" - Indicates a Bluetooth button press
186                #"AT+VGM=" - Indicates a microphone volume change
187                #"AT+VGS=" - Indicates a speakerphone volume change
188                #"AT+BRSF=" - The Headset is asking what features are supported
189                #"AT+CIND?" - The Headset is asking about the indicators that are signaled
190                #"AT+CIND=?" - The Headset is asking about the test indicators
191                #"AT+CMER=" - The Headset is asking which indicates are registered for updates
192                #"ATA" - When an incoming call has been answered, usually a Bluetooth button press
193                #"AT+CHUP" - When a call has been hung up, usually a Bluetooth button press
194                #"ATD>" - The Headset is requesting the local device to perform a memory dial
195                #"ATD" - The Headset is requesting to dial the number
196                #"AT+BLDN" - The Headset is requesting to perform last number dialed
197                #"AT+CCWA=" - The Headset has enabled call waiting
198                #"AT+CLIP=" - The Headset has enabled CLI (Calling Line Identification)
199                #"AT+VTS=" - The Headset is asking to send DTMF digits
200                #"AT+CHLD=" - The Headset is asking to put the call on Hold
201                #"AT+BVRA=" - The Headset is requesting voice recognition
202                #"ATH" - Call hang-up
203               
204                #self.device.write('\x29')
205                #self.device.write('AT+BRSF=24\r\n')
206               
207                buffer = ''
208               
209                while True:
210                        reply = self.device.read()
211                       
212                        if (len(reply) != 0):
213                                if DEBUG > 1:
214                                        print reply
215                                buffer += reply
216                       
217                        if buffer == "AT+BRSF=24\r":
218                                print "--> Received:",
219                                print buffer
220                                response = '\r\nOK\r\n'
221                                print "<-- Sending:",
222                                print response.replace('\r\n', '')
223                                self.device.write(response)
224                                buffer = ''
225                       
226                        elif buffer == 'AT+CIND=?\r':
227                                print "--> Received:",
228                                print buffer
229                                # first field indicates that we have cellular service [0-1]
230                                # second field indicates that we're in a call (0 for false) [0-1]
231                                # third field indicates the current call setup (0 for idle) [0-3]
232                                response = '\r\n+CIND: 1,0,0\r\n'
233                                print "<-- Sending:",
234                                print response.replace('\r\n', '')
235                                self.device.write(response)
236                                response = '\r\nOK\r\n'
237                                print "<-- Sending:",
238                                print response.replace('\r\n', '')
239                                self.device.write(response)
240                                buffer = ''
241                       
242                        elif buffer == 'AT+CMER=3, 0, 0, 1\r':
243                                print "--> Received:",
244                                print buffer
245                                response = '\r\nOK\r\n'
246                                print "<-- Sending:",
247                                print response.replace('\r\n', '')
248                                self.device.write(response)
249                                response = '\r\n+CIEV:2,1\r\n'
250                                print "<-- Sending:",
251                                print response.replace('\r\n', '')
252                                self.device.write(response)
253                                response = '\r\n+CIEV:3,0\r\n'
254                                print "<-- Sending:",
255                                print response.replace('\r\n', '')
256                                self.device.write(response)
257                                buffer = ''
258                       
259                        elif buffer == 'AT+VGS=15\r':
260                                print "--> Received:",
261                                print buffer
262                                response = '\r\nOK\r\n'
263                                print "<-- Sending:",
264                                print response.replace('\r\n', '')
265                                self.device.write(response)
266                                buffer = ''
267                       
268                        elif buffer == 'AT+VGM=08\r':
269                                print "--> Received:",
270                                print buffer
271                                response = '\r\nOK\r\n'
272                                print "<-- Sending:",
273                                print response.replace('\r\n', '')
274                                self.device.write(response)
275                                buffer = ''
276                               
277                                self.device.close()
278                                sys.exit()
279       
280       
281        ##################################################################
282       
283        def process_data_row(self, row, extended_code_level):
284               
285                pass
286       
287       
288        ##################################################################
289       
290        def process_data_payload(self, data_payload):
291               
292                '''A DataRow consists of bytes in the following format:
293                ([EXCODE]...) [CODE] ([VLENGTH])   [VALUE...]
294                ____________________ ____________ ___________
295                ^^^^(Value Type)^^^^ ^^(length)^^ ^^(value)^^'''
296               
297                if self.DEBUG > 1:
298                        print "data payload:",
299                        for byte in data_payload:
300                                print byte.encode("hex"),
301                        print
302               
303                byte_index = 0
304               
305                # Parse the extended_code_level, code, and length
306                while (byte_index < len(data_payload)):
307                        extended_code_level = 0
308                       
309                        # 1. Parse and count the number of [EXCODE] (0x55)
310                        #    bytes that may be at the beginning of the
311                        #    current DataRow.
312                        while (data_payload[byte_index] == PROTOCOL_EXCODE):
313                                extended_code_level += 1
314                                byte_index += 1
315                       
316                        # 2. Parse the [CODE] byte for the current DataRow.
317                        code = data_payload[byte_index]
318                        byte_index += 1
319                        code = code.encode("hex")
320                       
321                        # 3. If [CODE] >= 0x80, parse the next byte as the
322                        #    [VLENGTH] byte for the current DataRow.
323                        if (code > '\x7f'.encode("hex")):
324                                length = data_payload[byte_index]
325                                byte_index += 1
326                                length = length.encode("hex")
327                                length = int(length, 16)
328                        else:
329                                length = 1
330                       
331                        #TODO: Based on the extendedCodeLevel, code, length,
332                        #and the [CODE] Definitions Table, handle the next
333                        #"length" bytes of data from the payload as
334                        #appropriate for your application.
335                       
336                        print "EXCODE level:",
337                        print extended_code_level,
338                        print " CODE:",
339                        print code,
340                        print " length:",
341                        print length
342                       
343                        data_values = ''
344                        value_index = 0
345                       
346                        # 4. Parse and handle the [VALUE...] byte(s) of the current
347                        #    DataRow, based on the DataRow's [EXCODE] level, [CODE],
348                        #    and [VLENGTH] (refer to the Code De nitions Table).
349                        while value_index < length:
350                                # Uh-oh more C mojo
351                                value = data_payload[(byte_index + value_index)] # & 0xFF
352                                data_values += value.encode("hex")
353                                value_index += 1
354                       
355                        print "Data Values:",
356                        print data_values
357                        print
358                       
359                        byte_index += length
360                       
361                        # 5. If not all bytes have been parsed from the payload[] array,
362                        # return to step 1. to continue parsing the next DataRow.
363       
364       
365        ##################################################################
366       
367        def process_packet(self, packet):
368               
369                '''Each Packet begins with its Header, followed by its Data Payload,
370                and ends with the Payload's Check-sum Byte, as follows:
371                [SYNC] [SYNC] [PLENGTH]      [PAYLOAD...]         [CHKSUM]
372                _______________________      _____________     ____________
373                ^^^^^^^^(Header)^^^^^^^      ^^(Payload)^^     ^(Checksum)^'''
374               
375                valid_length = False
376                valid_checksum = False
377               
378                if self.DEBUG > 1:
379                        print packet
380               
381               
382                # SPEC: [PLENGTH] byte indicates the length, in bytes, of the
383                # Packet's Data Payload [PAYLOAD...] section, and may be any value
384                # from 0 up to 169. Any higher value indicates an error
385                # (PLENGTH TOO LARGE). Be sure to note that [PLENGTH] is the length
386                # of the Packet's Data Payload, NOT of the entire Packet.
387                # The Packet's complete length will always be [PLENGTH] + 4.
388
389                packet_length = packet[2]
390                packet_length = packet_length.encode("hex")
391                packet_length = int(packet_length, 16)
392               
393                if ((packet_length <= 169) and \
394                         (packet_length + 4) == (len(packet))):
395                       
396                        if self.DEBUG > 1:
397                                print "packet length correct"
398                       
399                        valid_length = True
400               
401                else:
402                        if self.DEBUG:
403                                print "ERROR: packet length bad"
404               
405               
406                if valid_length:
407                       
408                        data_payload = packet[3:-1]
409                       
410                        # SPEC: The [CHKSUM] Byte must be used to verify the integrity of the
411                        # Packet's Data Payload. The Payload's Checksum is defined as:
412                        #  1. summing all the bytes of the Packet's Data Payload
413                        #  2. taking the lowest 8 bits of the sum
414                        #  3. performing the bit inverse (one's compliment inverse)
415                        #     on those lowest 8 bits
416                       
417                        packet_checksum = packet[-1]
418                        packet_checksum = packet_checksum.encode("hex")
419                        packet_checksum = int(packet_checksum, 16)
420                       
421                        payload_checksum = 0
422                        for byte in data_payload:
423                                value = byte.encode("hex")
424                                value = int(value, 16)
425                                payload_checksum += value
426                       
427                        # Take the lowest 8 bits of the calculated payload_checksum
428                        # and invert them. Serious C code mojo.
429                        payload_checksum &= 0xff
430                        payload_checksum = ~payload_checksum & 0xff
431                       
432                       
433                        if packet_checksum != payload_checksum:
434                                if self.DEBUG > 1:
435                                        print "ERROR: packet checksum does not match"
436                                        print "       packet_checksum:",
437                                        print packet_checksum
438                                        print "       payload_checksum:",
439                                        print payload_checksum
440                       
441                        else:
442                                valid_checksum = True
443                                if self.DEBUG > 1:
444                                        print "packet checksum correct"
445                                       
446                                self.process_data_payload(data_payload)
447               
448               
449                return(valid_length, valid_checksum)
450       
451       
452        ##################################################################
453       
454        def process_byte(self, byte):
455               
456                self.buffer += byte
457                self.byte_count += 1
458               
459                if (len(self.buffer) > 2):
460                        if self.buffer[-2:] == '\xAA\xAA':
461                                # New packet header found
462                               
463                                (valid_length, valid_checksum) = self.process_packet(self.buffer[:-2])
464                               
465                                if ((valid_length) or \
466                                         (len(self.buffer) > 173)):
467                                        # If processing the packet returned valid checks then we
468                                        # restart reading the buffer to examine new packages.
469                                        # However if the current buffer size is larger than 173 bytes
470                                        # (the maximum possible packet length according to the protocol
471                                        # specification document, then we still want to reset the buffer
472                                        # because an error has occured in the stream)
473                                        self.buffer = '\xAA\xAA'
474                                        self.packet_count += 1
475               
476               
477                elif self.buffer == "AT+BRSF=24\r":
478                        # This string is received when connecting to the wrong
479                        # Bluetooth serial device channel of the ThinkGear device
480                        if self.DEBUG:
481                                print "--> Received:",
482                                print self.buffer
483                        response = '\r\nOK\r\n'
484                        if self.DEBUG:
485                                print "<-- Sending:",
486                                print response.replace('\r\n', '')
487                        self.device.write(response)
488                        self.buffer = ''
489                       
490                        if self.DEBUG:
491                                print "ERROR: Serial device connected to wrong channel"
492                                print "(Consider changing from channel 1 to channel 3",
493                                print " or another COM port for example)"
494                       
495                        self.device.close()
496                        sys.exit()
497               
498               
499                if ((self.DEBUG > 1) and \
500                         ((self.byte_count >= DEBUG_BYTE_COUNT) or \
501                          (self.packet_count >= DEBUG_PACKET_COUNT))):
502                        if self.DEBUG:
503                                print "max debugging count reached, disconnecting"
504                        self.device.close()
505                        sys.exit()
506       
507       
508        ##################################################################
509       
510        def start(self):
511               
512                self.buffer = ''
513                self.packet_count = 0
514                self.byte_count = 0
515               
516                while True:
517                       
518                        byte = self.device.read()
519                       
520                        if (len(byte) != 0):
521                                if DEBUG > 2:
522                                        print byte
523                                       
524                                self.process_byte(byte)
525
526
527#####################################################################
528#####################################################################
529
530        def read_response(self, modem):
531
532                # This function is passed to the modem each time the software
533                # has asked the modem to perform a particular function.
534                # It looks for an "OK" message from the modem, and stores all
535                # data read back from the modem in a line, one entry per response
536                # line. The function returns that list of responses, as well as
537                # a status flag indicating whether the modem timed out while
538                # waiting for the "OK" response. The timeout gets set during
539                # modem initialization, based on the value stored in the database
540
541                modem_timed_out = 0
542
543                #self.log.pdebug("Waiting for 'OK' from %s modem..." % self.modem_type)
544
545                response = []
546
547                while 'OK\r\n' not in response:
548                        reply = modem.readline()
549                        if (len(reply) == 0):
550                                #self.log.perror("Modem timeout has been exceeded")
551                                modem_timed_out = 1
552                                break # The timeout has been exceeded
553                        else:
554                                if (reply != '\r\n'):
555                                        log_reply = string.replace(reply, '\r', '')
556                                        log_reply = string.replace(log_reply, '\n', '')
557                                        #self.log.pdebug(log_reply)
558                                response.append(reply)
559
560                #self.log.debug(response)
561                if self.DEBUG:
562                        print "DEBUG:",
563                        print response
564
565                return (response, modem_timed_out)
566
567
568        ##################################################################
569
570        def test_modem(self, modem):
571
572                # This function simply sends an "AT" command to the modem
573                # and checks for a reponse. It is useful for verifying
574                # that the modem is attached, configured, and functioning correctly
575
576                modem.write("AT\r")
577                (response, modem_timed_out) = read_response(modem)
578
579
580#####################################################################
581# Main
582#####################################################################
583
584if __name__ == '__main__':
585       
586        # Perform correct KeyboardInterrupt handling
587        #signal.signal(signal.SIGINT, signal.SIG_DFL)
588       
589        #log = puzzlebox_logger.puzzlebox_logger(logfile='server_thinkgear')
590        log = None
591       
592        # Collect default settings and command line parameters
593        serial_port = DEFAULT_SERIAL_PORT
594       
595        for each in sys.argv:
596               
597                if each.startswith("--port="):
598                        serial_port = each[ len("--port="): ]
599       
600       
601        #app = QtCore.QCoreApplication(sys.argv)
602       
603        server = puzzlebox_thinkgear_serial_protocol(log, \
604                                                           serial_port, \
605                                                           DEBUG=DEBUG)
606       
607        #sys.exit(app.exec_())
608       
609        server.start()
610       
611       
Note: See TracBrowser for help on using the repository browser.