Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions experiment/Speller/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .stimulus import SSVEPStimulus
from .grid import SpellerGrid
65 changes: 65 additions & 0 deletions experiment/Speller/grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pygame
from .stimulus import SSVEPStimulus

class SpellerGrid:
def __init__(self, screen_res, frequencies, labels):
self.width, self.height = screen_res
self.stimuli = []

# 1. ------------------------------------ Struktura Obiektu ------------------------------------

# (3 kolumny, 2 rzędy = 6 kafelków)
cols = 3
rows = 2

# Odstęp między kafelkami
margin = 80

# 2. ------------------------------------ Symetryczne ustawianie siatki ------------------------------------


# 2. Obliczamy maksymalny możliwy rozmiar kafelka, który wejdzie na ekran
available_w = self.width - (cols + 1) * margin
available_h = self.height - (rows + 1) * margin

tile_w = available_w // cols
tile_h = available_h // rows

# Rozmiar boku kwadratu (wybieramy mniejszy, by zachować proporcje 1:1)
size = min(tile_w, tile_h)

# Całkowita szerokość i wysokość całego boxa gridu
total_grid_width = (cols * size) + ((cols - 1) * margin)
total_grid_height = (rows * size) + ((rows - 1) * margin)


start_x = (self.width - total_grid_width) // 2
start_y = (self.height - total_grid_height) // 2

# 5. ------------------------------------ Konstrukcja kafelków z uwzględnieniem offsetu ------------------------------------
for i in range(len(labels)):
col = i % cols
row = i // cols

# Nowa pozycja z uwzględnieniem start_x i start_y
x = start_x + col * (size + margin)
y = start_y + row * (size + margin)

s = SSVEPStimulus(x, y, size, frequencies[i], label=labels[i])
self.stimuli.append(s)


def update(self):
for s in self.stimuli:
s.update()


def draw(self, surface):
for s in self.stimuli:
s.draw(surface)


def set_labels(self, new_labels):
for i, label in enumerate(new_labels):
if i < len(self.stimuli):
self.stimuli[i].label = label
31 changes: 31 additions & 0 deletions experiment/Speller/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import pygame
import config
from Speller import SSVEPStimulus

def draw_menu(screen, is_connected):
screen.fill((0, 0, 0))

btn_offline = SSVEPStimulus(x=config.WIDTH//2 - 350, y=config.HEIGHT//2 - 100,
size=300, freq=0, label="OFFLINE (Calibration)")

btn_online = SSVEPStimulus(x=config.WIDTH//2 + 50, y=config.HEIGHT//2 - 100,
size=300, freq=0, label="ONLINE\n(Speller)")

btn_offline.current_color = (0, 130, 0)
btn_online.current_color = (130, 0, 0)

btn_offline.draw(screen)
btn_online.draw(screen)

font = pygame.font.SysFont('Arial', 24)
text = font.render("Press 1 for Offline or 2 for Online", True, config.COLOR_WHITE)
screen.blit(text, (config.WIDTH//2 - text.get_width()//2, config.HEIGHT//2 + 250))

status_font = pygame.font.SysFont('Arial', 20, bold=True)
if is_connected:
status_text = status_font.render("Status: Połączono", True, config.COLOR_GREEN)
else:
status_text = status_font.render("Status: Nie połączono", True, (255, 0, 0))
screen.blit(status_text, (20, 20))

return btn_offline.rect, btn_online.rect
88 changes: 88 additions & 0 deletions experiment/Speller/stimulus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import math
import pygame

class SSVEPStimulus:
def __init__(self, x, y, size, freq, refresh_rate=60, label=""):
self.rect = pygame.Rect(x, y, size, size)
self.freq = freq
self.refresh_rate = refresh_rate
self.label = label
self.frame_count = 0

# Kolory
self.current_color = (0, 0, 0)
self.text_color = (255, 255, 255)

pygame.font.init()
self.font = pygame.font.SysFont('Arial', int(40), bold=True)

def update(self):
t = self.frame_count / self.refresh_rate

sine_val = math.sin(2 * math.pi * self.freq * t)

intensity = int(127.5 * (sine_val * 0.5 + 1.0))

self.current_color = (intensity, 0, 0)

self.frame_count += 1

def draw(self, surface):
# Renderowanie boxa
pygame.draw.rect(surface, self.current_color, self.rect)

# Renderowanie liter w boxie
if self.label:
# Margines wewnętrzny
padding_percent = 0.05
target_width = int(self.rect.width * (1 - padding_percent * 2))

# --- Algorytm zawijania tekstu ---

explicit_segments = self.label.split('\n')
final_lines = []

line_height = self.font.get_linesize()

# Przetwarzamy każdy segment
for segment in explicit_segments:

words = segment.split(' ')
if not words or (len(words) == 1 and words[0] == ''):
final_lines.append("")
continue

current_line_words = []

for word in words:
test_line_str = ' '.join(current_line_words + [word])

width, height = self.font.size(test_line_str)

if width <= target_width:

current_line_words.append(word)
else:

final_lines.append(' '.join(current_line_words))
current_line_words = [word]

final_lines.append(' '.join(current_line_words))

# --- Renderowanie i wyśrodkowanie ---

total_text_height = len(final_lines) * line_height

start_y = self.rect.centery - (total_text_height // 2)

# Renderujemy każdą linię po kolei
for i, line_str in enumerate(final_lines):
if not line_str: continue

line_surf = self.font.render(line_str, True, self.text_color)

line_rect = line_surf.get_rect(centerx=self.rect.centerx)

line_rect.y = start_y + i * line_height

surface.blit(line_surf, line_rect)
57 changes: 57 additions & 0 deletions experiment/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

# --- Ustawienia ekranu ---
WIDTH, HEIGHT = 1280, 720
REFRESH_RATE = 60

# --- Parametry SSVEP ---
# 6 częstotliwości
FREQS = [7.5, 8.57, 10.0, 12.0, 15.0, 8.0]

# --- Struktura Alfabetu (Drzewo) ---

ALPHABET_TREE = {
"root": [
"UNDO",
"START",
"A B C D\nE F G H\nI J K L\nM N O P",
"Q R S T\nU V W X\nY Z + -\n* / ( )",
"1 2 3 4\n5 6 7 8\n9 0",
"$ & @ \"\n. , ? !\n% : ; =\n~ # Del Blank"
],

# Poddrzewo Grupy 1 (Litery A-P)
"A B C D\nE F G H\nI J K L\nM N O P": [
"BACK", "MAIN",
"A B C D", "E F G H",
"I J K L", "M N O P"
],

# Poddrzewo Grupy 2 (Litery Q-Z i symbole)
"Q R S T\nU V W X\nY Z + -\n* / ( )": [
"BACK", "MAIN",
"Q R S T", "U V W X",
"Y Z + -", "* / ( )"
],

# Poddrzewo Grupy 3 (Cyfry)
"1 2 3 4\n5 6 7 8\n9 0": [
"BACK", "MAIN",
"1 2", "3 4",
"5 6", "7 8 9 0"
],

# Liście (konkretne litery) - przykład dla pierwszej grupy
"A B C D": ["BACK", "MAIN", "A", "B", "C", "D"],
"E F G H": ["BACK", "MAIN", "E", "F", "G", "H"],
"I J K L": ["BACK", "MAIN", "I", "J", "K", "L"],
"M N O P": ["BACK", "MAIN", "M", "N", "O", "P"],

# Przykład dla cyfr
"1 2": ["BACK", "MAIN", "1", "2", "-", "-"],
}

TILE_MARGIN = 40

MENU_OPTIONS = ["1. OFFLINE", "2. ONLINE"]
COLOR_WHITE = (255, 255, 255)
COLOR_GRAY = (50, 50, 50)
63 changes: 63 additions & 0 deletions experiment/eeg_headset/eeg_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import numpy as np
import brainaccess.core as ba_core
from brainaccess.core.eeg_manager import EEGManager

class BrainAccessBackend:
def __init__(self, device_name="BA-MIDI", simulate=True):
self.simulate = simulate
self.device_name = device_name
self.data_buffer = []
self.connected = False

if not self.simulate:
ba_core.init()
self.eeg = EEGManager()
self.eeg.set_callback_chunk(self._on_chunk_received)

def _on_chunk_received(self, chunk):
self.data_buffer.append(chunk)

def start(self):
if self.simulate:
print("[Backend] Tryb testowy: Pomijam łączenie z czepkiem.")
self.connected = False
return

try:
self.eeg.connect(bt_device_name=self.device_name)
self.eeg.start_stream()
self.connected = True
print(f"[Backend] Połączono z {self.device_name}.")
except Exception as e:
print(f"[Backend] Błąd połączenia: {e}")
self.connected = False

def send_marker(self, label):
if self.simulate or not self.connected:
print(f"[Backend Mock] Wysłano marker: {label}")
return

try:
if self.eeg.is_connected():
self.eeg.annotate(label)
except Exception as e:
print(e)

def stop(self):
if self.simulate or not self.connected:
print("[Backend Mock] Koniec sesji.")
return

try:
if self.eeg.is_streaming():
self.eeg.stop_stream()
if self.eeg.is_connected():
self.eeg.disconnect()

if self.data_buffer:
np.save("eeg_session_data.npy", np.array(self.data_buffer, dtype=object))
except Exception as e:
print(e)

def is_device_connected(self):
return self.connected
Empty file removed experiment/gui/__init__.py
Empty file.
79 changes: 79 additions & 0 deletions experiment/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import pygame
import sys
import gc

from config import *
from Speller import SSVEPStimulus, SpellerGrid
from Speller import menu

from experiment.eeg_headset.eeg_handler import BrainAccessBackend

# 1. PyGame setup
pygame.init()
eeg_handler = BrainAccessBackend(device_name="BA-MIDI", simulate=False)

# 2. Ustawienia ekranu
screen = pygame.display.set_mode((WIDTH, HEIGHT), vsync=1)
pygame.display.set_caption("SSVEP Speller Experiment")

# 3. Zegar
clock = pygame.time.Clock()

# 4. Tworzymy obiekt
grid = SpellerGrid((WIDTH, HEIGHT), FREQS, ALPHABET_TREE["root"])

state = "MENU"

running = True

while running:
events = pygame.event.get()

for event in events:
gc.collect()

if event.type == pygame.QUIT:
running = False

if state == "MENU" and event.type == pygame.KEYDOWN:
if event.key == pygame.K_1:
gc.disable()

state = "OFFLINE"
print("Startujemy fazę kalibracji...")

eeg_handler.start()
eeg_handler.send_marker("START-PHASE-OFFLINE")

if event.key == pygame.K_2:
gc.disable()

state = "ONLINE"
print("Startujemy Speller...")

eeg_handler.start()
eeg_handler.send_marker("START-PHASE-ONLINE")


screen.fill((0, 0, 0))

if state == "MENU":
# Użycie bezpiecznej metody z klasy backendu
connected = eeg_handler.is_device_connected()
menu.draw_menu(screen, is_connected=connected)

if state == "OFFLINE":
grid.update()
grid.draw(screen)

if state == "ONLINE":
grid.update()
grid.draw(screen)

pygame.display.flip()

clock.tick(60)

eeg_handler.stop()
pygame.quit()
sys.exit()
Loading