"""
============
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"]
########################################################################
[docs]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)
# ----------------------------------------------------------------------
[docs] 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)
# ----------------------------------------------------------------------
[docs] def stop(self) -> None:
"""Stop pipeline execution."""
self.stop_pipeline()
if w.get_value('record'):
timer.set_timeout(self.stop_record, 2000)
# ----------------------------------------------------------------------
[docs] 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)]
# ----------------------------------------------------------------------
[docs] 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'}
# ----------------------------------------------------------------------
[docs] 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',
}
# ----------------------------------------------------------------------
[docs] 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
# ----------------------------------------------------------------------
[docs] 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
# ----------------------------------------------------------------------
[docs] 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()