From aebc16649a10448063f805538f87bd84dd71d12c Mon Sep 17 00:00:00 2001 From: i-tong <42049859+i-tong@users.noreply.github.com> Date: Fri, 16 Apr 2021 11:24:22 -0700 Subject: [PATCH] Added support for Gazepoint biometrics kit Added data includes blinks, pupil size (mm), dial, GSR, heart rate, heart rate pulse signal, and TTL. --- opengaze.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 186 insertions(+), 23 deletions(-) diff --git a/opengaze.py b/opengaze.py index ac80a97..b9e41c2 100644 --- a/opengaze.py +++ b/opengaze.py @@ -6,6 +6,7 @@ # Version 1 (27-Apr-2016) import os +import re import copy import time import socket @@ -58,8 +59,8 @@ def __init__(self, ip='127.0.0.1', port=4242, logfile='default.tsv', \ # Open a new debug file. if self._debug: dt = time.strftime("%Y-%m-%d_%H-%M-%S") - self._debuglog = open('debug_%s.txt' % (dt), 'w') - self._debuglog.write("OPENGAZE PYTHON DEBUG LOG %s\n" % (dt)) + self._debuglog = open('debug_{}.txt'.format(dt), 'w') + self._debuglog.write("OPENGAZE PYTHON DEBUG LOG {}\n".format(dt)) self._debugcounter = 0 self._debugconsolidatefreq = 100 @@ -70,7 +71,7 @@ def __init__(self, ip='127.0.0.1', port=4242, logfile='default.tsv', \ # Start a new TCP/IP socket. It is curcial that it has a timeout, # as timeout exceptions will be handled gracefully, and are in fact # necessary to prevent the incoming Thread from freezing. - self._debug_print("Connecting to %s (%s)..." % (self.host, self.port)) + self._debug_print("Connecting to {} ({})...".format(self.host, self.port)) self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.connect((self.host, self.port)) self._sock.settimeout(1.0) @@ -86,7 +87,7 @@ def __init__(self, ip='127.0.0.1', port=4242, logfile='default.tsv', \ self._current_calibration_point = None # LOGGING - self._debug_print("Opening new logfile '%s'" % (logfile)) + self._debug_print("Opening new logfile '{}'".format(logfile)) # Open a new log file. self._logfile = open(logfile, 'w') # Write the header to the log file. @@ -100,6 +101,13 @@ def __init__(self, ip='127.0.0.1', port=4242, logfile='default.tsv', \ 'LEYEX', 'LEYEY', 'LEYEZ', 'LPUPILD', 'LPUPILV', \ 'REYEX', 'REYEY', 'REYEZ', 'RPUPILD', 'RPUPILV', \ 'CX', 'CY', 'CS', \ + 'BKID', 'BKDUR', 'BKPMIN', \ + 'LPMM', 'LPMMV', 'RPMM', 'RPMMV', \ + 'DIAL', 'DIALV', \ + 'GSR', 'GSRV', \ + 'HR', 'HRV', \ + 'HRP', \ + 'TTL0', 'TTL1', 'TTLV', \ 'USER'] self._n_logvars = len(self._logheader) self._logfile.write('\t'.join(self._logheader) + '\n') @@ -190,6 +198,13 @@ def __init__(self, ip='127.0.0.1', port=4242, logfile='default.tsv', \ self.enable_send_pupil_right(True) self.enable_send_time(True) self.enable_send_time_tick(True) + self.enable_send_blink(True) + self.enable_send_pupilmm(True) + self.enable_send_dial(True) + self.enable_send_gsr(True) + self.enable_send_heart_rate(True) + self.enable_send_heart_rate_pulse(True) + self.enable_send_ttl(True) self.enable_send_user_data(True) # Reset the user-defined variable. self.user_data("0") @@ -305,8 +320,8 @@ def stop_recording(self): def _debug_print(self, msg): if self._debug: - self._debuglog.write('%s: %s\n' % \ - (datetime.datetime.now().strftime("%H:%M:%S.%f"), msg)) + self._debuglog.write('{}: {}\n'.format( \ + datetime.datetime.now().strftime("%H:%M:%S.%f"), msg)) if self._debugcounter % self._debugconsolidatefreq == 0: self._debuglog.flush() os.fsync(self._debuglog.fileno()) @@ -315,11 +330,11 @@ def _debug_print(self, msg): def _format_msg(self, command, ID, values=None): # Create the start of the formatted string. - xml = '<%s ID="%s" ' % (command.upper(), ID.upper()) + xml = '<{} ID="{}" '.format(command.upper(), ID.upper()) # Add the values for each parameter. if values: for par, val in values: - xml += '%s="%s" ' % (par.upper(), val) + xml += '{}="{}" '.format(par.upper(), val) # Add the ending. xml += '/>\r\n' @@ -347,6 +362,16 @@ def _log_sample(self, sample): def _parse_msg(self, xml): +# # Fix for GazePoint API bug. +# if xml == '': +# xml = '' + + # Attempt to fix all malformed XML strings. (GazePoint frequently + # manages to send malformed XML messages, which causes an error for + # lxml decoding.) + xml = re.sub(r'(=".+?")', r'\1 ', xml) + + # Parse the xml string. e = lxml.etree.fromstring(xml) return (e.tag, e.attrib) @@ -393,6 +418,8 @@ def _process_incoming(self): timeout = False try: instring = self._sock.recv(self._maxrecvsize) + instring = instring.decode("utf-8") + except socket.timeout: timeout = True # Get a received timestamp. @@ -405,7 +432,7 @@ def _process_incoming(self): self._debug_print("socket recv timeout") continue - self._debug_print("Raw instring: %r" % (instring)) + self._debug_print(r"Raw instring: {}".format(instring)) # Split the messages (they are separated by '\r\n'). messages = instring.split('\r\n') @@ -423,7 +450,7 @@ def _process_incoming(self): # Run through all messages. for msg in messages: - self._debug_print("Incoming: %r" % (msg)) + self._debug_print(r"Incoming: {}".format(msg)) # Parse the message. command, msgdict = self._parse_msg(msg) # Check if the incoming message is an acknowledgement. @@ -482,14 +509,14 @@ def _process_outgoing(self): # Break the while loop. break - self._debug_print("Outgoing: %r" % (msg)) + self._debug_print(r"Outgoing: {}".format(msg)) # Lock the socket to prevent other Threads from simultaneously # accessing it. self._socklock.acquire() # Send the command to the OpenGaze Server. t = time.time() - self._sock.send(msg) + self._sock.send(msg.encode("utf-8")) # Unlock the socket again. self._socklock.release() @@ -515,7 +542,7 @@ def _send_message(self, command, ID, values=None, \ while (not acknowledged) and (not timeout): # Add the command to the outgoing Queue. - self._debug_print("Outqueue add: %r" % (msg)) + self._debug_print(r"Outqueue add: {}".format(msg)) self._outqueue.put(msg) # Wait until an acknowledgement comes in. @@ -532,8 +559,7 @@ def _send_message(self, command, ID, values=None, \ if msg in self._outlatest.keys(): t = copy.copy(self._outlatest[msg]) sent = True - self._debug_print("Outqueue sent: %r" \ - % (msg)) + self._debug_print(r"Outqueue sent: {}".format(msg)) self._outlock.release() time.sleep(0.001) @@ -546,8 +572,7 @@ def _send_message(self, command, ID, values=None, \ if ID in self._acknowledgements.keys(): if self._acknowledgements[ID] >= t: acknowledged = True - self._debug_print("Outqueue acknowledged: %r" \ - % (msg)) + self._debug_print(r"Outqueue acknowledged: {}".format(msg)) self._acklock.release() time.sleep(0.001) @@ -866,6 +891,144 @@ def enable_send_cursor(self, state): # Return a success Boolean. return acknowledged and (timeout==False) + def enable_send_blink(self, state): + + """Enable (state=True) or disable (state=False) the inclusion of + data on the blinks in the data record string. This data + consists of the following: + BKID: Each blink is assigned an ID value and incremented by one. + The BKID value equals 0 for every record where no blink has been + detected. + BKDUR: The duration of the preceding blink in seconds. + BKPMIN: The number of blinks in the previous 60 second period of time. + """ + + # Send the message (returns after the Server acknowledges receipt). + acknowledged, timeout = self._send_message('SET', \ + 'ENABLE_SEND_BLINK', \ + values=[('STATE', int(state))], \ + wait_for_acknowledgement=True) + + # Return a success Boolean. + return acknowledged and (timeout==False) + + def enable_send_pupilmm(self, state): + + """Enable (state=True) or disable (state=False) the inclusion of + data on pupil diameter in the data record string. This data + consists of the following: + LPMM: The diameter of the left eye pupil in millimeters. + LPMMV: The valid flag with value of 1 if the data is valid, and 0 + if it is not. + RPMM: The diameter of the right eye pupil in millimeters. + RPMMV: The valid flag with value of 1 if the data is valid, and 0 + if it is not. + """ + + # Send the message (returns after the Server acknowledges receipt). + acknowledged, timeout = self._send_message('SET', \ + 'ENABLE_SEND_PUPILMM', \ + values=[('STATE', int(state))], \ + wait_for_acknowledgement=True) + + # Return a success Boolean. + return acknowledged and (timeout==False) + + def enable_send_dial(self, state): + + """Enable (state=True) or disable (state=False) the inclusion of + data on the biometrics analog self-reporting dial value in the data + record string. This data consists of the following: + DIAL: The dial value from 0 to 1. + DIALV: The valid flag with value of 1 if the data is valid, and 0 + if it is not + """ + + # Send the message (returns after the Server acknowledges receipt). + acknowledged, timeout = self._send_message('SET', \ + 'ENABLE_SEND_DIAL', \ + values=[('STATE', int(state))], \ + wait_for_acknowledgement=True) + + # Return a success Boolean. + return acknowledged and (timeout==False) + + def enable_send_gsr(self, state): + + """Enable (state=True) or disable (state=False) the inclusion of + data on the biometrics galvanic skin response resistance in the data + record string. This data consists of the following: + GSR: The skin resistance in ohms. + GSRV: The valid flag with value of 1 if the data is valid, and 0 + if it is not. + """ + + # Send the message (returns after the Server acknowledges receipt). + acknowledged, timeout = self._send_message('SET', \ + 'ENABLE_SEND_GSR', \ + values=[('STATE', int(state))], \ + wait_for_acknowledgement=True) + + # Return a success Boolean. + return acknowledged and (timeout==False) + + def enable_send_heart_rate(self, state): + + """Enable (state=True) or disable (state=False) the inclusion of + data on biometrics heart rate in the data record string. This data + consists of the following: + HR: The heart rate in BPM. + HRV: The valid flag with value of 1 if the data is valid, and 0 + if it is not. + """ + + # Send the message (returns after the Server acknowledges receipt). + acknowledged, timeout = self._send_message('SET', \ + 'ENABLE_SEND_HR', \ + values=[('STATE', int(state))], \ + wait_for_acknowledgement=True) + + # Return a success Boolean. + return acknowledged and (timeout==False) + + def enable_send_heart_rate_pulse(self, state): + + """Enable (state=True) or disable (state=False) the inclusion of + data on the biometrics heart rate pulse signal in the data record + string. This data consists of the following: + HRP: The heart rate pulse signal. + """ + + # Send the message (returns after the Server acknowledges receipt). + acknowledged, timeout = self._send_message('SET', \ + 'ENABLE_SEND_HR_PULSE', \ + values=[('STATE', int(state))], \ + wait_for_acknowledgement=True) + + # Return a success Boolean. + return acknowledged and (timeout==False) + + def enable_send_ttl(self, state): + + """Enable (state=True) or disable (state=False) the inclusion of + data on the biometrics analog (TTL0) and digital (TTL1) Input/Output + channels. in the data record string. This data consists of the + following: + TTL0: The analog value of channel 0 (0-1023). + TTL1: The digital value of channel 1 (0-1). + TTLV: The valid flag with value of 1 if the sensor cable is connected, + and 0 if it is not. + """ + + # Send the message (returns after the Server acknowledges receipt). + acknowledged, timeout = self._send_message('SET', \ + 'ENABLE_SEND_TTL', \ + values=[('STATE', int(state))], \ + wait_for_acknowledgement=True) + + # Return a success Boolean. + return acknowledged and (timeout==False) + def enable_send_user_data(self, state): """Enable (state=True) or disable (state=False) the inclusion of @@ -1041,12 +1204,12 @@ def get_calibration_points(self): if acknowledged: points = [] self._inlock.acquire() - for i in range(self._incoming['ACK']['CALIBRATE_ADDPOINT']['PTS']): + for i in range(int(self._incoming['ACK']['CALIBRATE_ADDPOINT']['PTS'])): points.append( \ + (copy.copy(float( \ + self._incoming['ACK']['CALIBRATE_ADDPOINT']['X{}'.format(int(i+1))])), \ copy.copy(float( \ - self._incoming['ACK']['CALIBRATE_ADDPOINT']['X%d' % i+1])), \ - copy.copy(float( \ - self._incoming['ACK']['CALIBRATE_ADDPOINT']['Y%d' % i+1])) \ + self._incoming['ACK']['CALIBRATE_ADDPOINT']['Y{}'.format(int(i+1))]))) \ ) self._inlock.release() @@ -1103,9 +1266,9 @@ def get_calibration_result(self): p = {} for par in params: if par in ['LV', 'RV']: - p['%s' % (par)] = cal['%s%d' % (par, i)] == '1' + p['{}'.format(par)] = cal['{}{}'.format(par, i)] == '1' else: - p['%s' % (par)] = float(cal['%s%d' % (par, i)]) + p['{}'.format(par)] = float(cal['{}{}'.format(par, i)]) points.append(copy.deepcopy(p)) self._inlock.release()