<commit_before>"""
Core OpenBCI object for handling connections and samples from the board.

EXAMPLE USE:

def handle_sample(sample):
    print(sample.channel_data)

board = OpenBCIBoard()
board.print_register_settings()
board.start_streaming(handle_sample)

NOTE: If daisy modules is enabled, the callback will occur every two samples, hence "packet_id" will only contain even
numbers. As a side effect, the sampling rate will be divided by 2.

FIXME: at the moment we can just force daisy mode, do not check that the module is detected.
TODO: enable impedance

"""

import serial
import struct
import numpy as np
import time
import timeit
import atexit
import logging
import threading
import sys
import pdb
import glob

SAMPLE_RATE = 250.0     # Hz
START_BYTE = 0xA0       # start of data packet
END_BYTE = 0xC0         # end of data packet
ADS1299_Vref = 4.5      # reference voltage for ADC in ADS1299.  set by its hardware
ADS1299_gain = 24.0     # assumed gain setting for ADS1299.  set by its Arduino code
scale_fac_uVolts_per_count = ADS1299_Vref / float((pow(2, 23) - 1)) / ADS1299_gain * 1000000.
scale_fac_accel_G_per_count = 0.002 / (pow(2, 4))  # assume set to +/4G, so 2 mG

'''
Commands for in SDK http://docs.openbci.com/software/01-Open BCI_SDK:

command_stop = "s"
command_startText = "x"
command_startBinary = "b"
command_startBinary_wAux = "n"
command_startBinary_4chan = "v"
command_activateFilters = "F"
command_deactivateFilters = "g"

command_deactivate_channel = {"1", "2", "3", "4", "5", "6", "7", "8"}           # 1-8 (on keyboard)
command_activate_channel = {"q", "w", "e", "r", "t", "y", "u", "i"}             # q-i (on keyboard)

command_activate_leadoffP_channel = {"!", "@", "#", "$", "%", "^", "&", "*"}    # shift + 1-8 (on keyboard)
command_deactivate_leadoffP_channel = {"Q", "W", "E", "R", "T", "Y", "U", "I"}  # shift + q-i (on keyboard)

command_activate_leadoffN_channel = {"A", "S", "D", "F", "G", "H", "J", "K"}    # shift + a-k (on keyboard)
command_deactivate_leadoffN_channel = {"Z", "X", "C", "V", "B", "N", "M", "<"}  # shift + z-, (on keyboard)

command_biasAuto = "`"
command_biasFixed = "~"
'''


class OpenBCIBoard(object):
    """ Handle a connection to an OpenBCI board.

    Args:
        port: The port to connect to.
        baud: The baud of the serial connection.
        daisy: Enable or disable daisy module and 16 chans readings
        aux, impedance: unused, for compatibility with ganglion API
    """

    def __init__(self, port=None, baud=115200, filter_data=True,
                 scaled_output=True, daisy=False, aux=False, impedance=False,
                 log=True, timeout=None):

        self.log = log  # print_incoming_text needs log
        self.streaming = False
        self.baudrate = baud
        self.timeout = timeout
        if not port:
            port = self.find_port()
        self.port = port
        # might be handy to know API
        self.board_type = "cyton"
        print("Connecting to V3 at port %s" % port)
        self.ser = serial.Serial(port=port, baudrate=baud, timeout=timeout)

        print("Serial established...")

        time.sleep(2)
        # Initialize 32-bit board, doesn't affect 8bit board
        self.ser.write(b'v')

        # wait for device to be ready
        time.sleep(1)
        self.print_incoming_text()

        self.streaming = False
        self.filtering_data = filter_data
        self.scaling_output = scaled_output
        self.eeg_channels_per_sample = 8  # number of EEG channels per sample *from the board*
        self.aux_channels_per_sample = 3  # number of AUX channels per sample *from the board*
        self.imp_channels_per_sample = 0  # impedance check not supported at the moment
        self.read_state = 0
        self.daisy = daisy
        self.last_odd_sample = OpenBCISample(-1, [], [])  # used for daisy
        self.log_packet_count = 0
        self.attempt_reconnect = False
        self.last_reconnect = 0
        self.reconnect_freq = 5
        self.packets_dropped = 0

        # Disconnects from board when terminated
        atexit.register(self.disconnect)

    def getBoardType(self):
        """ Returns the version of the board """
        return self.board_type

    def setImpedance(self, flag):
        """ Enable/disable impedance measure. Not implemented at the moment on Cyton. """
        return

    def ser_write(self, b):
        """Access serial port object for write"""
        self.ser.write(b)

    def ser_read(self):
        """Access serial port object for read"""
        return self.ser.read()

    def ser_inWaiting(self):
        """Access serial port object for inWaiting"""
        return self.ser.inWaiting()

    def getSampleRate(self):
        if self.daisy:
            return SAMPLE_RATE / 2
        else:
            return SAMPLE_RATE

    def getNbEEGChannels(self):
        if self.daisy:
            return self.eeg_channels_per_sample * 2
        else:
            return self.eeg_channels_per_sample

    def getNbAUXChannels(self):
        return self.aux_channels_per_sample

    def getNbImpChannels(self):
        return self.imp_channels_per_sample

    def start_streaming(self, callback, lapse=-1, biosignals=None):
        """ Start handling streaming data from the board. Call a provided callback
        for every single sample that is processed (every two samples with daisy module).

        :param callback: A callback function -- or a list of functions -- that will receive a single
        argument of the OpenBCISample object captured.
        :param lapse: optional cap on streaming time
        :param biosignals: biosignal object
        """
        if not self.streaming:
            self.ser.write(b'b')
            self.streaming = True

        start_time = timeit.default_timer()

        # Enclose callback funtion in a list if it comes alone
        if not isinstance(callback, list):
            callback = [callback]

        # Initialize check connection
        self.check_connection()

        while self.streaming:

            # read current sample
            sample = self._read_serial_binary()
            # if a daisy module is attached, wait to concatenate two samples (main board + daisy)
            if self.daisy:
                # odd sample: daisy sample, save for later
                if ~sample.id % 2:
                    self.last_odd_sample = sample
                # even sample: concatenate and send if last sample was the fist part, otherwise drop the packet
                elif sample.id - 1 == self.last_odd_sample.id:
                    # aux data is average between the two samples, as the channel samples are averaged by the board
                    avg_aux_data = list((np.array(sample.aux_data) + np.array(
                        self.last_odd_sample.aux_data)) / 2)
                    whole_sample = OpenBCISample(sample.id,
                                                 sample.channel_data + self.last_odd_sample.channel_data,
                                                 avg_aux_data)
                    for call in callback:
                        if biosignals is not None:
                            call(whole_sample, biosignals)
                        else:
                            call(whole_sample)
            else:
                for call in callback:
                    if biosignals is not None:
                        call(sample, biosignals)
                    else:
                        call(sample)

            # stop streaming after lapse amount of time
            if timeit.default_timer() - start_time > lapse > 0:
                self.stop()
            if self.log:
                self.log_packet_count = self.log_packet_count + 1

    """

        PARSER:
        Parses incoming data packet into OpenBCISample.
        Incoming Packet Structure:
        Start Byte(1)|Sample ID(1)|Channel Data(24)|Aux Data(6)|End Byte(1)
        0xA0|0-255|8, 3-byte signed ints|3 2-byte signed ints|0xC0

    """

    def _read_serial_binary(self, max_bytes_to_skip=3000):
        """ Main process: unpacks bytes from self.ser and creates OpenBCISample object from unpacked bytes.
        See incoming packet structure above. Warns user if unpacked data is unexpected.

        :param max_bytes_to_skip: allows this many bytes to be dropped before function stops operating
        :return: OpenBCISample object
        """

        def read(n):
            """ Reads n bytes from self.ser and returns them """
            bb = self.ser.read(n)

            # adds sanity check to serial.read() -- makes sure self.ser contains bytes to read
            if not bb:
                self.warn('Device appears to be stalled. Quitting...')
                sys.exit()
                raise Exception('Device Stalled')
                sys.exit()
                return '\xFF'
            else:
                return bb

        # TODO: use more efficient implementation of for loop
        for rep in range(max_bytes_to_skip):

            # ---------Start Byte & ID---------
            # reads bytes until it finds START_BYTE, warns client if there are bytes before, and starts
            # process at START_BYTE
            if self.read_state == 0:
                b = read(1)
                if struct.unpack('B', b)[0] == START_BYTE:

                    # found rep bytes before START_BYTE
                    if rep != 0:
                        self.warn('Skipped %d bytes before start found' % rep)
                        rep = 0

                    packet_id = struct.unpack('B', read(1))[0]  # packet id goes from 0-255
                    log_bytes_in = str(packet_id)

                    # start at START_BYTE, regardless of if bytes before
                    self.read_state = 1

            # ---------Channel Data---------
            # reads from struct and adds channel data to channel in order of channel
            elif self.read_state == 1:
                channel_data = []
                for c in range(self.eeg_channels_per_sample):

                    # 3-byte ints
                    literal_read = read(3)

                    unpacked = struct.unpack('3B', literal_read)
                    log_bytes_in = log_bytes_in + '|' + str(literal_read)

                    # 3-byte int in 2's complement
                    if unpacked[0] > 127:
                        pre_fix = bytes(bytearray.fromhex('FF'))
                    else:
                        pre_fix = bytes(bytearray.fromhex('00'))

                    literal_read = pre_fix + literal_read

                    # unpack little endian(>) signed integer(i) (makes unpacking platform independent)
                    myInt = struct.unpack('>i', literal_read)[0]

                    # myInt is data for individual channel -- added to channel_data in channel order
                    if self.scaling_output:
                        channel_data.append(myInt * scale_fac_uVolts_per_count)
                    else:
                        channel_data.append(myInt)

                self.read_state = 2

            # ---------Accelerometer Data---------
            # same process as above but instead adding accelerometer data to aux_data
            elif self.read_state == 2:
                aux_data = []
                for a in range(self.aux_channels_per_sample):

                    # short = h
                    acc = struct.unpack('>h', read(2))[0]
                    log_bytes_in = log_bytes_in + '|' + str(acc)

                    if self.scaling_output:
                        aux_data.append(acc * scale_fac_accel_G_per_count)
                    else:
                        aux_data.append(acc)

                self.read_state = 3

            # ---------End Byte---------
            # creates OpenBCISample from channel_data and aux_data and returns it and detects if packets
            # are dropped (ie. if last byte is not END_BYTE, data must have been lost)
            elif self.read_state == 3:
                val = struct.unpack('B', read(1))[0]
                log_bytes_in = log_bytes_in + '|' + str(val)
                self.read_state = 0  # read next packet

                if val == END_BYTE:
                    sample = OpenBCISample(packet_id, channel_data, aux_data)
                    self.packets_dropped = 0
                    return sample
                else:
                    self.warn("ID:<%d> <Unexpected END_BYTE found <%s> instead of <%s>" % (packet_id, val, END_BYTE))
                    logging.debug(log_bytes_in)
                    self.packets_dropped = self.packets_dropped + 1

    """

        Clean Up (atexit)
<commit_msg>optimization: converted START_BYTE to Byte object<commit_after>"""
Core OpenBCI object for handling connections and samples from the board.

EXAMPLE USE:

def handle_sample(sample):
    print(sample.channel_data)

board = OpenBCIBoard()
board.print_register_settings()
board.start_streaming(handle_sample)

NOTE: If daisy modules is enabled, the callback will occur every two samples, hence "packet_id" will only contain even
numbers. As a side effect, the sampling rate will be divided by 2.

FIXME: at the moment we can just force daisy mode, do not check that the module is detected.
TODO: enable impedance

"""

import serial
import struct
import numpy as np
import time
import timeit
import atexit
import logging
import threading
import sys
import pdb
import glob

SAMPLE_RATE = 250.0     # Hz
START_BYTE = b'0xA0'    # start of data packet
END_BYTE = 0xC0         # end of data packet
ADS1299_Vref = 4.5      # reference voltage for ADC in ADS1299.  set by its hardware
ADS1299_gain = 24.0     # assumed gain setting for ADS1299.  set by its Arduino code
scale_fac_uVolts_per_count = ADS1299_Vref / float((pow(2, 23) - 1)) / ADS1299_gain * 1000000.
scale_fac_accel_G_per_count = 0.002 / (pow(2, 4))  # assume set to +/4G, so 2 mG

'''
Commands for in SDK http://docs.openbci.com/software/01-Open BCI_SDK:

command_stop = "s"
command_startText = "x"
command_startBinary = "b"
command_startBinary_wAux = "n"
command_startBinary_4chan = "v"
command_activateFilters = "F"
command_deactivateFilters = "g"

command_deactivate_channel = {"1", "2", "3", "4", "5", "6", "7", "8"}           # 1-8 (on keyboard)
command_activate_channel = {"q", "w", "e", "r", "t", "y", "u", "i"}             # q-i (on keyboard)

command_activate_leadoffP_channel = {"!", "@", "#", "$", "%", "^", "&", "*"}    # shift + 1-8 (on keyboard)
command_deactivate_leadoffP_channel = {"Q", "W", "E", "R", "T", "Y", "U", "I"}  # shift + q-i (on keyboard)

command_activate_leadoffN_channel = {"A", "S", "D", "F", "G", "H", "J", "K"}    # shift + a-k (on keyboard)
command_deactivate_leadoffN_channel = {"Z", "X", "C", "V", "B", "N", "M", "<"}  # shift + z-, (on keyboard)

command_biasAuto = "`"
command_biasFixed = "~"
'''


class OpenBCIBoard(object):
    """ Handle a connection to an OpenBCI board.

    Args:
        port: The port to connect to.
        baud: The baud of the serial connection.
        daisy: Enable or disable daisy module and 16 chans readings
        aux, impedance: unused, for compatibility with ganglion API
    """

    def __init__(self, port=None, baud=115200, filter_data=True,
                 scaled_output=True, daisy=False, aux=False, impedance=False,
                 log=True, timeout=None):

        self.log = log  # print_incoming_text needs log
        self.streaming = False
        self.baudrate = baud
        self.timeout = timeout
        if not port:
            port = self.find_port()
        self.port = port
        # might be handy to know API
        self.board_type = "cyton"
        print("Connecting to V3 at port %s" % port)
        self.ser = serial.Serial(port=port, baudrate=baud, timeout=timeout)

        print("Serial established...")

        time.sleep(2)
        # Initialize 32-bit board, doesn't affect 8bit board
        self.ser.write(b'v')

        # wait for device to be ready
        time.sleep(1)
        self.print_incoming_text()

        self.streaming = False
        self.filtering_data = filter_data
        self.scaling_output = scaled_output
        self.eeg_channels_per_sample = 8  # number of EEG channels per sample *from the board*
        self.aux_channels_per_sample = 3  # number of AUX channels per sample *from the board*
        self.imp_channels_per_sample = 0  # impedance check not supported at the moment
        self.read_state = 0
        self.daisy = daisy
        self.last_odd_sample = OpenBCISample(-1, [], [])  # used for daisy
        self.log_packet_count = 0
        self.attempt_reconnect = False
        self.last_reconnect = 0
        self.reconnect_freq = 5
        self.packets_dropped = 0

        # Disconnects from board when terminated
        atexit.register(self.disconnect)

    def getBoardType(self):
        """ Returns the version of the board """
        return self.board_type

    def setImpedance(self, flag):
        """ Enable/disable impedance measure. Not implemented at the moment on Cyton. """
        return

    def ser_write(self, b):
        """Access serial port object for write"""
        self.ser.write(b)

    def ser_read(self):
        """Access serial port object for read"""
        return self.ser.read()

    def ser_inWaiting(self):
        """Access serial port object for inWaiting"""
        return self.ser.inWaiting()

    def getSampleRate(self):
        if self.daisy:
            return SAMPLE_RATE / 2
        else:
            return SAMPLE_RATE

    def getNbEEGChannels(self):
        if self.daisy:
            return self.eeg_channels_per_sample * 2
        else:
            return self.eeg_channels_per_sample

    def getNbAUXChannels(self):
        return self.aux_channels_per_sample

    def getNbImpChannels(self):
        return self.imp_channels_per_sample

    def start_streaming(self, callback, lapse=-1, biosignals=None):
        """ Start handling streaming data from the board. Call a provided callback
        for every single sample that is processed (every two samples with daisy module).

        :param callback: A callback function -- or a list of functions -- that will receive a single
        argument of the OpenBCISample object captured.
        :param lapse: optional cap on streaming time
        :param biosignals: biosignal object
        """
        if not self.streaming:
            self.ser.write(b'b')
            self.streaming = True

        start_time = timeit.default_timer()

        # Enclose callback funtion in a list if it comes alone
        if not isinstance(callback, list):
            callback = [callback]

        # Initialize check connection
        self.check_connection()

        while self.streaming:

            # read current sample
            sample = self._read_serial_binary()
            # if a daisy module is attached, wait to concatenate two samples (main board + daisy)
            if self.daisy:
                # odd sample: daisy sample, save for later
                if ~sample.id % 2:
                    self.last_odd_sample = sample
                # even sample: concatenate and send if last sample was the fist part, otherwise drop the packet
                elif sample.id - 1 == self.last_odd_sample.id:
                    # aux data is average between the two samples, as the channel samples are averaged by the board
                    avg_aux_data = list((np.array(sample.aux_data) + np.array(
                        self.last_odd_sample.aux_data)) / 2)
                    whole_sample = OpenBCISample(sample.id,
                                                 sample.channel_data + self.last_odd_sample.channel_data,
                                                 avg_aux_data)
                    for call in callback:
                        if biosignals is not None:
                            call(whole_sample, biosignals)
                        else:
                            call(whole_sample)
            else:
                for call in callback:
                    if biosignals is not None:
                        call(sample, biosignals)
                    else:
                        call(sample)

            # stop streaming after lapse amount of time
            if timeit.default_timer() - start_time > lapse > 0:
                self.stop()
            if self.log:
                self.log_packet_count = self.log_packet_count + 1

    """

        PARSER:
        Parses incoming data packet into OpenBCISample.
        Incoming Packet Structure:
        Start Byte(1)|Sample ID(1)|Channel Data(24)|Aux Data(6)|End Byte(1)
        0xA0|0-255|8, 3-byte signed ints|3 2-byte signed ints|0xC0

    """

    def _read_serial_binary(self, max_bytes_to_skip=3000):
        """ Main process: unpacks bytes from self.ser and creates OpenBCISample object from unpacked bytes.
        See incoming packet structure above. Warns user if unpacked data is unexpected.

        :param max_bytes_to_skip: allows this many bytes to be dropped before function stops operating
        :return: OpenBCISample object
        """

        def read(n):
            """ Reads n bytes from self.ser and returns them """
            bb = self.ser.read(n)

            # adds sanity check to serial.read() -- makes sure self.ser contains bytes to read
            if not bb:
                self.warn('Device appears to be stalled. Quitting...')
                sys.exit()
                raise Exception('Device Stalled')
                sys.exit()
                return '\xFF'
            else:
                return bb

        # TODO: use more efficient implementation of for loop
        for rep in range(max_bytes_to_skip):

            # ---------Start Byte & ID---------
            # reads bytes until it finds START_BYTE, warns client if there are bytes before, and starts
            # process at START_BYTE
            if self.read_state == 0:
                b = read(1)
                if b == START_BYTE:

                    # found rep bytes before START_BYTE
                    if rep != 0:
                        self.warn('Skipped %d bytes before start found' % rep)
                        rep = 0

                    packet_id = struct.unpack('B', b)[0]  # packet id goes from 0-255
                    log_bytes_in = str(packet_id)

                    # start at START_BYTE, regardless of if bytes before
                    self.read_state = 1

            # ---------Channel Data---------
            # reads from struct and adds channel data to channel in order of channel
            elif self.read_state == 1:
                channel_data = []
                for c in range(self.eeg_channels_per_sample):

                    # 3-byte ints
                    literal_read = read(3)

                    unpacked = struct.unpack('3B', literal_read)
                    log_bytes_in = log_bytes_in + '|' + str(literal_read)

                    # 3-byte int in 2's complement
                    if unpacked[0] > 127:
                        pre_fix = bytes(bytearray.fromhex('FF'))
                    else:
                        pre_fix = bytes(bytearray.fromhex('00'))

                    literal_read = pre_fix + literal_read

                    # unpack little endian(>) signed integer(i) (makes unpacking platform independent)
                    myInt = struct.unpack('>i', literal_read)[0]

                    # myInt is data for individual channel -- added to channel_data in channel order
                    if self.scaling_output:
                        channel_data.append(myInt * scale_fac_uVolts_per_count)
                    else:
                        channel_data.append(myInt)

                self.read_state = 2

            # ---------Accelerometer Data---------
            # same process as above but instead adding accelerometer data to aux_data
            elif self.read_state == 2:
                aux_data = []
                for a in range(self.aux_channels_per_sample):

                    # short = h
                    acc = struct.unpack('>h', read(2))[0]
                    log_bytes_in = log_bytes_in + '|' + str(acc)

                    if self.scaling_output:
                        aux_data.append(acc * scale_fac_accel_G_per_count)
                    else:
                        aux_data.append(acc)

                self.read_state = 3

            # ---------End Byte---------
            # creates OpenBCISample from channel_data and aux_data and returns it and detects if packets
            # are dropped (ie. if last byte is not END_BYTE, data must have been lost)
            elif self.read_state == 3:
                val = struct.unpack('B', read(1))[0]
                log_bytes_in = log_bytes_in + '|' + str(val)
                self.read_state = 0  # read next packet

                if val == END_BYTE:
                    sample = OpenBCISample(packet_id, channel_data, aux_data)
                    self.packets_dropped = 0
                    return sample
                else:
                    self.warn("ID:<%d> <Unexpected END_BYTE found <%s> instead of <%s>" % (packet_id, val, END_BYTE))
                    logging.debug(log_bytes_in)
                    self.packets_dropped = self.packets_dropped + 1

    # temporary -- unneeded if START_BYTE is Byte object
    def unicode_to_byte(byte_to_convert):
        """Converts START_BYTE or END_BYTE to Python's Byte object

        :param byte_to_convert:
        :return: Byte
        """
        return int(self.START_BYTE.encode('hex'), 16)

    """

        Clean Up (atexit)

{'fraction_non_alphanumeric': 0.09066822520942919, 'fraction_numerical': 0.012546269238262225, 'mean_word_length': 1.8272747301167658, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '654770', 'n_tokens_mistral': 7581, 'n_tokens_neox': 6943, 'n_words': 2212}