bci_framework.default_extensions.P300_Speller.main — BCI-Framework documentation

# Source code for bci_framework.default_extensions.P300_Speller.main

    """
    ============
    P300 Speller
    ============

    """

    from bci_framework.extensions.stimuli_delivery import StimuliAPI
    from bci_framework.extensions.stimuli_delivery.utils import Widgets as w
    from bci_framework.extensions.stimuli_delivery.utils import Tone as t

    from browser import document, html, timer
    import random
    import logging
    from typing import List

    CHARACTERS = "ABCDEF😂GHIJKL😍MNOPQR😘STUVWX😭YZ0123⎵456789⌫", 7

    CHARACTERS_SRLZ = ["A","B","C","D","E","F","aa",
                       "G","H","I","J","K","L","bb",
                       "M","N","O","P","Q","R","cc",
                       "S","T","U","V","W","X","dd",
                       "Y","Z","0","1","2","3","ee",
                       "4","5","6","7","8","9","ff"]

    ########################################################################
    class P300Speller(StimuliAPI):
        """Classic P300 speller."""

        # ----------------------------------------------------------------------
        def __init__(self, *args, **kwargs):
            """"""
            super().__init__(*args, **kwargs)
            self.add_stylesheet('styles.css')

            self.stimuli_area.style = {'background-color': 'black'}

            self.build_speller()
            self.build_grid()
            self.listen_feedbacks(self.on_feedback)

            self.dashboard <= w.label('P300 Speller', 'headline4')
            self.dashboard <= w.switch(
                label='Activate feedback speller',
                checked=False,
                on_change=self.show_speller,
                id='speller',
            )
            self.dashboard <= w.slider(
                label='Trials:',
                min=-1,
                max=15,
                value=5,
                step=1,
                discrete=True,
                marks=True,
                id='trials'
            )
            self.dashboard <= w.slider(
                label='Target notice:',
                min=500,
                max=5000,
                value=2000,
                step=100,
                unit='ms',
                id='notice'
            )
            self.dashboard <= w.slider(
                label='Flash duration:',
                min=100,
                max=500,
                value=125,
                step=5,
                unit='ms',
                id='duration'
            )
            self.dashboard <= w.slider(
                label='Inter stimulus interval:',
                min=10,
                max=500,
                value=62.5,
                step=5,
                unit='ms',
                id='inter_stimulus'
            )
            self.dashboard <= w.switch(
                label='Record EEG',
                checked=False,
                id='record',
            )
            self.dashboard <= w.switch(
                label='External marker synchronizer',
                checked=False,
                on_change=self.synchronizer,
                id='record',
            )

            self.dashboard <= w.button('Start run', on_click=self.start)
            self.dashboard <= w.button('Stop run', on_click=self.stop)

        # ----------------------------------------------------------------------
        def start(self) -> None:
            """Start the run.

            A run consist in a consecutive pipeline trials execution.
            """
            if w.get_value('record'):
                self.start_record()

            self.build_trials()
            timer.set_timeout(lambda: self.run_pipeline(
                self.pipeline_trial, self.trials), 2000)

        # ----------------------------------------------------------------------
        def stop(self) -> None:
            """Stop pipeline execution."""
            self.stop_pipeline()
            if w.get_value('record'):
                timer.set_timeout(self.stop_record, 2000)

        # ----------------------------------------------------------------------
        def build_trials(self) -> None:
            """Define the `trials` and `pipeline trials`.

            The `trials` consist (in this case) in a list of cues.
            The `pipeline trials` is a set of couples `(callable, duration)` that
            define a single trial, this list of functions are executed asynchronously
            and repeated for each trial.
            """
            self.propagate_seed()
            inter_stimulus = w.get_value('inter_stimulus')
            duration = w.get_value('duration')
            chars, ncols = CHARACTERS
            nrows = len(chars) // ncols

            self.trials = []
            trials = w.get_value('trials')
            if trials == -1:
                trials = 99
            for _ in range(trials):
                stimuli_array = []
                for i in range(ncols):
                    stimuli_array.append(f'.col-{i}')
                for i in range(nrows):
                    stimuli_array.append(f'.row-{i}')

                random.shuffle(stimuli_array)

                self.trials.append({'target': random.choice(CHARACTERS_SRLZ),
                                    'array': stimuli_array})

            random.shuffle(self.trials)

            if not w.get_value('speller'):
                self.pipeline_trial = [
                    (lambda ** kwargs: None, 500),
                    (self.target_notice, w.get_value('notice')),
                    (self.inter_stimulus, 3000)
                ]
            else:
                self.pipeline_trial = []

            [self.pipeline_trial.extend(
                [(self.activate, 300),
                 (self.inter_stimulus, 300)]) for _ in range(nrows+ncols)]

        # ----------------------------------------------------------------------
        def target_notice(self, target: str) -> None:
            """Highlight the character that's subject must focus on."""
            # self.send_marker(f"TARGET:{target}")
            target = document.select_one(f".p300-{target}-")
            target.style = {'color': '#00ff00',
                            'opacity': 0.5, 'font-weight': 'bold'}

        # ----------------------------------------------------------------------
        def inter_stimulus(self) -> None:
            """Remove the highlight over the focus character."""
            for element in document.select('.p300-char'):
                element.style = {'opacity': 0.3,
                                 'color': '#ffffff',
                                 'font-weight': 'normal',
                                 }

        # ----------------------------------------------------------------------
        def activate(self, array: List[str], target: str) -> None:
            """Highlight a column or a row."""

            selector = array.pop(0)
            elements = document.select(selector)
            # self.send_marker(f"ERP:{selector[1:]}")

            if target in [e.attrs['char'] for e in elements]:
                self.send_marker("TARGET")
            else:
                self.send_marker("NO-TARGET")

            for element in elements:
                element.style = {'opacity': 1,
                                 'font-weight': 'bold',
                                 }

        # ----------------------------------------------------------------------
        def show_speller(self, visible) -> None:
            """"""
            if visible:
                self.speller.style = {'display': 'block'}
                self.last_value = w.component['trials'].mdc.getValue()
                w.component['trials'].mdc.setValue(-1)
                document.select_one('#value_trials').html = ' infinite'

            else:
                self.speller.style = {'display': 'none'}
                w.component['trials'].mdc.setValue(self.last_value)
                document.select_one('#value_trials').html = f' {self.last_value}'

        # ----------------------------------------------------------------------
        def build_speller(self) -> None:
            """"""
            self.speller = html.INPUT(Class="p300-speller", readonly=True, style={'display': 'none'})
            self.speller.value = '_'
            self.stimuli_area <= self.speller

        # ----------------------------------------------------------------------
        def build_grid(self) -> None:
            """Create the grid with the letters."""
            table = html.TABLE(CLass='p300')
            tr = html.TR()
            table <= tr

            characters_train, ncols = CHARACTERS

            for i, char in enumerate(characters_train):
                char_print = CHARACTERS_SRLZ[i]
                row = i // ncols
                col = i % ncols
                if row == 6:
                    style = {"font-size": "6vh"}
                else:
                    style = {}
                td = html.TD(
                    char, Class=f'p300-char p300-{char_print}- col-{col} row-{row}', style=style)
                td.attrs['char'] = char_print
                tr <= td
                if i != 0 and not (i + 1) % ncols:
                    tr = html.TR()
                    table <= tr
            self.stimuli_area <= table

        # ----------------------------------------------------------------------
        def synchronizer(self, value: bool) -> None:
            """Show or hide synchronizer."""
            if value:
                self.show_synchronizer()
            else:
                self.hide_synchronizer()

        # ----------------------------------------------------------------------
        def on_feedback(self, value) -> None:
            """"""
            print(value)
            # if w.get_value('speller'):
                # self.speller.value = self.speller.value[:-1] + value + '_'

    if __name__ == '__main__':
        P300Speller()