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" + + + +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" + + + +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" + + + +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" + + + +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" + + + +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" + + + +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