diff --git a/README.md b/README.md index 1d96121e..b3a7b4a3 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,20 @@ Chords- Python is an open-source bag of tools designed to interface with Micro-c - Open command prompt and run: ```bash -python -m venv venv +python -m venv .venv ``` ```bash -venv\Scripts\activate # For Windows -source venv/bin/activate # For MacOS/Linux +.venv\Scripts\activate # For Windows +source .venv/bin/activate # For MacOS/Linux ``` +[!IMPORTANT] +You may get an execution policy error if scripts are restricted. To fix it, run: +```bash +Set-ExecutionPolicy Unrestricted -Scope Process +``` + ```bash pip install chordspy ``` @@ -38,9 +44,52 @@ chordspy ``` **Web Interface Preview**: -![Web Interface Screenshot](./chordspy/media/Interface.png) +![Web Interface Screenshot](Chords-Python\chordspy\media\Interface.png) + +![Web Interface Screenshot](Chords-Python\chordspy\media\Webinterface.png) + +# [!Optional] + +If you want to run the individual scripts, then follow these steps: + +- Open command prompt and run: +```bash +python -m venv .venv +``` + +```bash +.venv\Scripts\activate # For Windows +source .venv/bin/activate # For MacOS/Linux +``` + +[!IMPORTANT] +You may get an execution policy error if scripts are restricted. To fix it, run: +```bash +Set-ExecutionPolicy Unrestricted -Scope Process +``` + +```bash +pip install -r requirements.txt +``` + +## Usage +Run the command and access the web interface: +```bash +python -m chordspy.app +``` -![Web Interface Screenshot](./chordspy/media/Webinterface.png) +Run the command to start the LSL Stream only: +```bash +python -m chordspy.connection --protocol ble # For BLE +python -m chordspy.connection --protocol wifi # For WiFi +python -m chordspy.connection --protocol usb # For USB +``` + +Then, in a new terminal, run any application you need: +```bash +python -m chordspy.gui # For GUI +python -m chordspy.ffteeg # For EEG with FFT +``` ### Key Options: diff --git a/chordspy/config/apps.yaml b/chordspy/config/apps.yaml index 6d0df9f2..a8b4865d 100644 --- a/chordspy/config/apps.yaml +++ b/chordspy/config/apps.yaml @@ -60,4 +60,11 @@ apps: color: "teal" script: "csvplotter" description: "Load and plot data from CSV files for offline analysis." - category: "Tools" \ No newline at end of file + category: "Tools" + + - title: "Morse Decoder" + icon: "fa-font" + color: "rose" + script: "morse_decoder" + description: "Decode EOG Signals into Alphabets based on Morse Code." + category: "EOG" \ No newline at end of file diff --git a/chordspy/double_triple_blink.py b/chordspy/double_triple_blink.py new file mode 100644 index 00000000..88c0b814 --- /dev/null +++ b/chordspy/double_triple_blink.py @@ -0,0 +1,265 @@ +import numpy as np +from scipy.signal import butter, lfilter, lfilter_zi +from PyQt5.QtWidgets import QApplication, QVBoxLayout, QMainWindow, QWidget, QHBoxLayout +import pyqtgraph as pg +import pylsl +import sys +import time +from collections import deque + +class EOGMonitor(QMainWindow): + def __init__(self): + super().__init__() + + self.setWindowTitle("Real-Time EOG Monitor - Double & Triple Blink Detection") + self.setGeometry(100, 100, 800, 400) + + self.stream_active = True + self.last_data_time = None + + # Detection parameters (can be tuned) + self.min_interblink_gap = 0.1 # 100ms minimum between blinks + self.max_interblink_gap = 0.4 # 400ms maximum between blinks + self.double_triple_window = 500 # ms to wait after 2nd blink for possible triple + self.last_double_blink_time = 0 + self.last_triple_blink_time = 0 + + # State for look-ahead logic + self.blink_times = [] # list of (timestamp, index) + self.waiting_for_triple = False + self.triple_timer = None + self.locked = False # Prevents multiple detections per sequence + + # Create layout + layout = QVBoxLayout() + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + + # Create plot widget for EOG + self.eog_plot = pg.PlotWidget(self) + self.eog_plot.setBackground('w') + self.eog_plot.showGrid(x=True, y=True) + self.eog_plot.setMouseEnabled(x=False, y=False) + self.eog_plot.setTitle("Filtered EOG Signal (Low Pass: 10 Hz)") + + # Blink detection plot + self.blink_plot = pg.PlotWidget(self) + self.blink_plot.setBackground('w') + self.blink_plot.showGrid(x=True, y=True) + self.blink_plot.setYRange(0, 1) + self.blink_plot.setMouseEnabled(x=False, y=False) + self.blink_plot.setTitle("Blink Detection") + + # Add both plots to the layout + layout.addWidget(self.eog_plot) + layout.addWidget(self.blink_plot) + + # Set up LSL stream inlet + print("Searching for available LSL streams...") + available_streams = pylsl.resolve_streams() + + if not available_streams: + print("No LSL streams found! Exiting...") + sys.exit(0) + + self.inlet = None + for stream in available_streams: + try: + self.inlet = pylsl.StreamInlet(stream) + print(f"Connected to LSL stream: {stream.name()}") + break + except Exception as e: + print(f"Failed to connect to {stream.name()}: {e}") + + if self.inlet is None: + print("Unable to connect to any LSL stream! Exiting...") + sys.exit(0) + + self.sampling_rate = int(self.inlet.info().nominal_srate()) + print(f"Sampling rate: {self.sampling_rate} Hz") + + self.buffer_size = self.sampling_rate * 5 # 5 seconds buffer for recent data + self.eog_data = np.zeros(self.buffer_size) + self.time_data = np.linspace(0, 5, self.buffer_size) + self.blink_data = np.zeros(self.buffer_size) # Blink data array + self.current_index = 0 + + # Low-pass filter for EOG (10 Hz) + self.b, self.a = butter(4, 10.0 / (0.5 * self.sampling_rate), btype='low') + self.zi = lfilter_zi(self.b, self.a) # Initialize filter state + + self.eog_plot.setXRange(0, 5, padding=0) + if self.sampling_rate == 250: + self.eog_plot.setYRange(0, 2**10, padding=0) + elif self.sampling_rate == 500: + self.eog_plot.setYRange(0, 5000, padding=0) + + # Plot curves + self.eog_curve = self.eog_plot.plot(self.time_data, self.eog_data, pen=pg.mkPen('b', width=1)) + self.blink_curve = self.blink_plot.plot(self.time_data, self.blink_data, pen=pg.mkPen('r', width=2)) + + # Circular buffer for detected peaks (store (index, time)) + self.detected_peaks = deque(maxlen=self.sampling_rate * 5) + + # Timer for plot update + self.timer = pg.QtCore.QTimer() + self.timer.timeout.connect(self.update_plot) + self.timer.start(15) + self.start_time = time.time() + + def update_plot(self): + samples, _ = self.inlet.pull_chunk(timeout=0.0, max_samples=30) + if samples: + self.last_data_time = time.time() + for sample in samples: + self.eog_data[self.current_index] = sample[0] + self.current_index = (self.current_index + 1) % self.buffer_size + + # Filter only the new data + filtered_eog, self.zi = lfilter(self.b, self.a, self.eog_data, zi=self.zi) + + # Update curve with the filtered EOG signal + self.eog_plot.clear() + self.eog_curve = self.eog_plot.plot(self.time_data, filtered_eog, pen=pg.mkPen('b', width=1)) + + if time.time() - self.start_time >= 2: + self.detect_blinks(filtered_eog) + + # Clear out old peaks from the circular buffer + current_time = time.time() + while self.detected_peaks and (current_time - self.detected_peaks[0][1] > 4): + self.detected_peaks.popleft() + + # Update the blink plot based on stored peaks + self.blink_data[:] = 0 + for index, _ in self.detected_peaks: + if 0 <= index < self.buffer_size: + self.blink_data[index] = 1 + + # Mark the stored peaks on the EOG plot + peak_indices = [index for index, t in self.detected_peaks] + peak_values = [filtered_eog[i] for i in peak_indices] + self.eog_plot.plot(self.time_data[peak_indices], peak_values, pen=None, symbol='o', symbolPen='r', symbolSize=6) + + # Update the blink plot + self.blink_curve.setData(self.time_data, self.blink_data) + else: + if self.last_data_time and (time.time() - self.last_data_time) > 2: + self.stream_active = False + print("LSL stream disconnected!") + self.timer.stop() + self.close() + + def detect_blinks(self, filtered_eog): + if self.locked: + return + mean_signal = np.mean(filtered_eog) + stdev_signal = np.std(filtered_eog) + threshold = mean_signal + (1.5 * stdev_signal) + + window_size = 1 * self.sampling_rate + start_index = self.current_index - window_size + if start_index < 0: + start_index = 0 + end_index = self.current_index + + filtered_window = filtered_eog[start_index:end_index] + peaks = self.detect_peaks(filtered_window, threshold) + + for peak in peaks: + full_peak_index = start_index + peak + peak_time = time.time() - (self.current_index - full_peak_index) / self.sampling_rate + self.detected_peaks.append((full_peak_index, peak_time)) + self.handle_new_blink(peak_time, full_peak_index, filtered_eog) + + def handle_new_blink(self, peak_time, peak_index, filtered_eog): + if self.locked: + return + # Remove old blinks (older than 1.5s) + self.blink_times = [(t, idx) for t, idx in self.blink_times if peak_time - t < 1.5] + self.blink_times.append((peak_time, peak_index)) + + if self.waiting_for_triple: + # If already waiting for triple, check if this is the 3rd blink + if len(self.blink_times) >= 3: + t1, idx1 = self.blink_times[-3] + t2, idx2 = self.blink_times[-2] + t3, idx3 = self.blink_times[-1] + gap1 = t2 - t1 + gap2 = t3 - t2 + if (self.min_interblink_gap <= gap1 <= self.max_interblink_gap and + self.min_interblink_gap <= gap2 <= self.max_interblink_gap): + # Triple blink detected + self.locked = True + print("TRIPLE BLINK DETECTED!") + self.last_triple_blink_time = time.time() + self.eog_plot.plot([self.time_data[idx1], self.time_data[idx2], self.time_data[idx3]], + [filtered_eog[idx1], filtered_eog[idx2], filtered_eog[idx3]], + pen=None, symbol='x', symbolPen='g', symbolSize=12) + self.reset_blink_state() + return + else: + # Not waiting for triple, check for double + if len(self.blink_times) >= 2: + t1, idx1 = self.blink_times[-2] + t2, idx2 = self.blink_times[-1] + gap = t2 - t1 + if self.min_interblink_gap <= gap <= self.max_interblink_gap: + # Start timer to wait for possible triple + self.waiting_for_triple = True + if self.triple_timer is not None: + self.triple_timer.stop() + self.triple_timer = pg.QtCore.QTimer() + self.triple_timer.setSingleShot(True) + self.triple_timer.timeout.connect(lambda: self.double_blink_timeout(filtered_eog)) + self.triple_timer.start(self.double_triple_window) + + def double_blink_timeout(self, filtered_eog): + if self.locked: + return + # Called if no 3rd blink appears in the window + if len(self.blink_times) >= 2: + t1, idx1 = self.blink_times[-2] + t2, idx2 = self.blink_times[-1] + gap = t2 - t1 + if self.min_interblink_gap <= gap <= self.max_interblink_gap: + self.locked = True + print("DOUBLE BLINK DETECTED!") + self.last_double_blink_time = time.time() + self.eog_plot.plot([self.time_data[idx1], self.time_data[idx2]], [filtered_eog[idx1], filtered_eog[idx2]], pen=None, symbol='x', symbolPen='b', symbolSize=10) + self.reset_blink_state() + + def reset_blink_state(self): + self.blink_times = [] + self.waiting_for_triple = False + if self.triple_timer is not None: + self.triple_timer.stop() + self.triple_timer = None + # Unlock after a short refractory period + pg.QtCore.QTimer.singleShot(500, self.unlock) + + def unlock(self): + self.locked = False + + def detect_peaks(self, signal, threshold): + peaks = [] + prev_peak_time = None + min_peak_gap = 0.1 # Minimum time gap between two peaks in seconds + for i in range(1, len(signal) - 1): + if signal[i] > signal[i - 1] and signal[i] > signal[i + 1] and signal[i] > threshold: + current_peak_time = i / self.sampling_rate + if prev_peak_time is not None: + time_gap = current_peak_time - prev_peak_time + if time_gap < min_peak_gap: + continue + peaks.append(i) + prev_peak_time = current_peak_time + return peaks + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = EOGMonitor() + print("Note: There will be a 2s calibration delay before peak detection starts.") + window.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/chordspy/morse_decoder.py b/chordspy/morse_decoder.py new file mode 100644 index 00000000..86c487d2 --- /dev/null +++ b/chordspy/morse_decoder.py @@ -0,0 +1,342 @@ +import numpy as np +from scipy.signal import butter, lfilter, lfilter_zi, iirnotch +import pylsl +import sys +import time +from collections import deque +import threading +import tkinter as tk + +class MorseCodeEOGSystem: + def __init__(self, gui_label=None, gui_root=None): + self.morse_code = { + '.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E', + '..-.': 'F', '--.': 'G', '....': 'H', '..': 'I', '.---': 'J', + '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O', + '.--.': 'P', '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T', + '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X', '-.--': 'Y', + '--..': 'Z', '-----': '0', '.----': '1', '..---': '2', '...--': '3', + '....-': '4', '.....': '5', '-....': '6', '--...': '7', '---..': '8', + '----.': '9' + } + + self.morse_buffer = "" # Output buffer for morse code + self.min_interblink_gap = 0.1 # 100ms minimum between blinks in a double blink + self.max_interblink_gap = 0.4 # 400ms maximum between blinks in a double blink + self.double_blink_cooldown = 1.0 # 1 second cooldown between double blink detections + self.last_double_blink_time = 0 + self.last_data_time = None + self.stream_active = True + self.last_input_time = None # Track last input time for inactivity + self.inactivity_timeout = 3.0 + + # GUI label and root for updating decoded character + self.gui_label = gui_label + self.gui_root = gui_root + + # Accumulated decoded word for GUI + self.decoded_word = "" + + # Set up LSL stream inlet + print("Searching for available LSL streams...") + available_streams = pylsl.resolve_streams() + if not available_streams: + print("No LSL streams found! Exiting...") + sys.exit(0) + + self.inlet = None + for stream in available_streams: + try: + self.inlet = pylsl.StreamInlet(stream) + print(f"Connected to LSL stream: {stream.name()}") + break + except Exception as e: + print(f"Failed to connect to {stream.name()}: {e}") + + if self.inlet is None: + print("Unable to connect to any LSL stream! Exiting...") + sys.exit(0) + + self.sampling_rate = int(self.inlet.info().nominal_srate()) + print(f"Sampling rate: {self.sampling_rate} Hz") + + # Buffer for EOG data + self.buffer_size = self.sampling_rate * 5 # 5 seconds buffer + self.eog_data = np.zeros(self.buffer_size) + self.filtered_eog_data = np.zeros(self.buffer_size) # Buffer for filtered EOG data + self.current_index = 0 + + # Low-pass filter for blink detection (10 Hz) + self.b, self.a = butter(4, 10.0 / (0.5 * self.sampling_rate), btype='low') + self.zi = lfilter_zi(self.b, self.a) + + # Circular buffer for detected peaks + self.detected_peaks = deque(maxlen=self.sampling_rate * 5) + self.start_time = time.time() + + # Left/Right detection parameters + self.BUFFER_SIZE = 250 + self.BASELINE_SAMPLES = 100 + self.DEVIATION_SIGMA = 8 + self.MIN_MOVEMENT_SAMPLES = 8 + self.COOLDOWN_SAMPLES = 15 + + # Left/Right data structures + self.circular_buffer = deque(maxlen=self.BUFFER_SIZE) + self.baseline = None + self.baseline_std = None + self.current_state = "NEUTRAL" + self.movement_samples = 0 + self.cooldown_counter = 0 + self.last_movement = None + self.movement_sequence = deque(maxlen=4) + + # Filter parameters for left/right detection + self.NOTCH_FREQ = 50.0 + self.NOTCH_Q = 20.0 + self.BANDPASS_LOW = 1.0 + self.BANDPASS_HIGH = 20.0 + + # Initialize filters for left/right detection + self.initialize_filters() + + print("Right movement -> Dot (.)") + print("Left movement -> Dash (-)") + print("Double blink -> Process morse code and clear buffer") + print("=" * 50) + + def initialize_filters(self): + """Initialize filters for left/right detection""" + self.notch_b, self.notch_a = iirnotch(self.NOTCH_FREQ, self.NOTCH_Q, self.sampling_rate) + nyq = 0.5 * self.sampling_rate + low = self.BANDPASS_LOW / nyq + high = self.BANDPASS_HIGH / nyq + self.bandpass_b, self.bandpass_a = butter(2, [low, high], btype='band') + self.filter_state_notch = np.zeros(max(len(self.notch_a), len(self.notch_b)) - 1) + self.filter_state_bandpass = np.zeros(max(len(self.bandpass_a), len(self.bandpass_b)) - 1) + + def apply_filters(self, sample): + """Apply notch and bandpass filters to sample""" + if self.filter_state_notch[0] == -1: + filtered, self.filter_state_notch = lfilter(self.notch_b, self.notch_a, [sample], zi=None) + else: + filtered, self.filter_state_notch = lfilter(self.notch_b, self.notch_a, [sample], zi=self.filter_state_notch) + + if self.filter_state_bandpass[0] == -1: + filtered, self.filter_state_bandpass = lfilter(self.bandpass_b, self.bandpass_a, filtered, zi=None) + else: + filtered, self.filter_state_bandpass = lfilter(self.bandpass_b, self.bandpass_a, filtered, zi=self.filter_state_bandpass) + + return filtered[0] + + def update_baseline_stats(self): + """Update baseline statistics for left/right detection""" + self.baseline = np.median(self.circular_buffer) + self.baseline_std = np.std(self.circular_buffer) + print(f"Baseline set: {self.baseline:.2f}μV") + + def get_movement_type(self, current_value): + """Determine movement type based on current value""" + deviation = current_value - self.baseline + threshold = self.DEVIATION_SIGMA * self.baseline_std + + if deviation < -threshold: + return "LEFT" + elif deviation > threshold: + return "RIGHT" + else: + return "NEUTRAL" + + def check_movement_completion(self): + """Check if a complete movement sequence has been detected""" + if len(self.movement_sequence) != 4: + return False + + seq = tuple(self.movement_sequence) + + # Right movement -> Dot (.) + if seq == ("RIGHT", "NEUTRAL", "LEFT", "NEUTRAL"): + print(".", end="", flush=True) + self.morse_buffer += "." + self.movement_sequence.clear() + self.last_input_time = time.time() # Update last input time + return True + + # Left movement -> Dash (-) + if seq == ("LEFT", "NEUTRAL", "RIGHT", "NEUTRAL"): + print("-", end="", flush=True) + self.morse_buffer += "-" + self.movement_sequence.clear() + self.last_input_time = time.time() # Update last input time + return True + + self.movement_sequence.popleft() # If we have 4 elements but no match, clear the oldest one + return False + + def process_morse_code(self): + """Process the current morse buffer and convert to character""" + if self.morse_buffer in self.morse_code: + character = self.morse_code[self.morse_buffer] + print(f" -> {character}") + # Append character to the decoded word and update GUI + self.decoded_word += character + self.update_gui(self.decoded_word) + self.morse_buffer = "" + self.last_input_time = None # Reset last input time after decoding + else: + # print(f" -> Unknown morse code: {self.morse_buffer}") + self.morse_buffer = "" + self.last_input_time = None # Reset last input time after decoding + + def update_gui(self, text): + # Update the label in the tkinter window with the decoded word + if self.gui_label and self.gui_root: + def set_label(): + self.gui_label.config(text=text) + self.gui_root.after(0, set_label) + + def detect_peaks(self, signal, threshold): + """Detect peaks in the signal for blink detection""" + peaks = [] + prev_peak_time = None + min_peak_gap = 0.1 # Minimum time gap between two peaks in seconds + + for i in range(1, len(signal) - 1): + if signal[i] > signal[i - 1] and signal[i] > signal[i + 1] and signal[i] > threshold: + current_peak_time = i / self.sampling_rate + if prev_peak_time is not None: + time_gap = current_peak_time - prev_peak_time + if time_gap < min_peak_gap: + continue + peaks.append(i) + prev_peak_time = current_peak_time + + return peaks + + def detect_blinks(self, filtered_eog): + """Detect blinks and check for double blinks""" + mean_signal = np.mean(filtered_eog) + stdev_signal = np.std(filtered_eog) + threshold = mean_signal + (1.5 * stdev_signal) + + window_size = 1 * self.sampling_rate + start_index = self.current_index - window_size + if start_index < 0: + start_index = 0 + + end_index = self.current_index + filtered_window = filtered_eog[start_index:end_index] + peaks = self.detect_peaks(filtered_window, threshold) + + for peak in peaks: + full_peak_index = start_index + peak + peak_time = time.time() - (self.current_index - full_peak_index) / self.sampling_rate + self.detected_peaks.append((full_peak_index, peak_time)) + + # Double blink detection using actual peak times + if len(self.detected_peaks) >= 2: + last_peak_index, last_peak_time = self.detected_peaks[-1] + prev_peak_index, prev_peak_time = self.detected_peaks[-2] + time_diff = last_peak_time - prev_peak_time + + if (self.min_interblink_gap <= time_diff <= self.max_interblink_gap and + time.time() - self.last_double_blink_time > self.double_blink_cooldown): + + print("\nDOUBLE BLINK DETECTED!") + self.process_morse_code() + self.last_double_blink_time = time.time() + + def run(self): + try: + while self.stream_active: + samples, _ = self.inlet.pull_chunk(timeout=0.1, max_samples=10) + + if samples: + self.last_data_time = time.time() + + for sample in samples: + # Store data for blink detection (channel 1) + self.eog_data[self.current_index] = sample[1] + # Filter only the new sample and update filtered_eog_data + filtered_sample, self.zi = lfilter(self.b, self.a, [sample[1]], zi=self.zi) + self.filtered_eog_data[self.current_index] = filtered_sample[0] + self.current_index = (self.current_index + 1) % self.buffer_size + + # Process for left/right detection (channel 0) + filtered_sample_lr = sample[0] # Using raw signal for left/right + self.circular_buffer.append(filtered_sample_lr) + + # Set baseline for left/right detection + if len(self.circular_buffer) == self.BASELINE_SAMPLES and self.baseline is None: + self.update_baseline_stats() + continue + + if self.baseline is None: + continue + + # Left/Right movement detection + detected_state = self.get_movement_type(filtered_sample_lr) + + if self.cooldown_counter > 0: + self.cooldown_counter -= 1 + continue + + # Movement validation + if detected_state != "NEUTRAL": + if detected_state == self.last_movement or self.last_movement is None: + self.movement_samples += 1 + else: + self.movement_samples = 1 + + if self.movement_samples >= self.MIN_MOVEMENT_SAMPLES: + if detected_state != self.current_state: + self.movement_sequence.append(detected_state) + self.current_state = detected_state + self.last_movement = detected_state + self.cooldown_counter = self.COOLDOWN_SAMPLES + self.movement_samples = 0 + self.check_movement_completion() + else: + if self.current_state != "NEUTRAL": + self.movement_sequence.append("NEUTRAL") + self.current_state = "NEUTRAL" + self.check_movement_completion() + self.movement_samples = 0 + self.last_movement = None + + # Blink detection + if time.time() - self.start_time >= 2: + # Use only the filtered_eog_data buffer + self.detect_blinks(self.filtered_eog_data) + + # Clear out old peaks from the circular buffer + current_time = time.time() + while self.detected_peaks and (current_time - self.detected_peaks[0][1] > 4): + self.detected_peaks.popleft() + + else: + if self.last_data_time and (time.time() - self.last_data_time) > 2: + self.stream_active = False + print("LSL stream disconnected!") + if self.morse_buffer and self.last_input_time: + if time.time() - self.last_input_time > self.inactivity_timeout: + print(f"\nBuffer timeout. Clearing morse buffer: {self.morse_buffer}") + self.morse_buffer = "" + self.last_input_time = None + + except KeyboardInterrupt: + print("\nExiting...") + +if __name__ == "__main__": + gui_root = tk.Tk() + gui_root.title("Morse Output") + gui_root.configure(bg="white") + gui_root.geometry("500x300+220+280") + gui_label = tk.Label(gui_root, text="", font=("Arial", 48), bg="white", fg="black") + gui_label.pack(expand=True) + + system = MorseCodeEOGSystem(gui_label=gui_label, gui_root=gui_root) + system_thread = threading.Thread(target=system.run, daemon=True) + system_thread.start() + + gui_root.mainloop() \ No newline at end of file