From e4957f4d20d87ee82827e33739bf352bdff8380f Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 02:46:19 -0400 Subject: [PATCH 01/11] Add initial GUI --- app.py | 188 +++++++++++++++++++++++++++++++++++++++ assets/icon.png | Bin 0 -> 4753 bytes constants.py | 5 ++ gui.py | 205 ++++++++++++++++++++++++++++++++++++++++++ main.py | 227 ++--------------------------------------------- requirements.txt | 1 + util.py | 17 ++++ 7 files changed, 424 insertions(+), 219 deletions(-) create mode 100644 app.py create mode 100644 assets/icon.png create mode 100644 constants.py create mode 100644 gui.py create mode 100644 util.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..758514d --- /dev/null +++ b/app.py @@ -0,0 +1,188 @@ +"""pyBlasher CLI app.""" + +import time +from sys import exit + +import serial + +from flash_firmware import flash_image, pulse_nrst +from nor_flash_comm import ( + reset, + read_section, + save_hexdump, +) +from util import find_cp2102n_ports + +SERIAL_PORT = "COM1" + + +def __flash_image(): + print(f"1. Enter a firmware filepath (.bin):") + image_path = input("> ") + if len(image_path) < 5 or image_path[-4:] != ".bin": + image_path += ".bin" + + print(f"2. Opening serial port ({SERIAL_PORT})") + with serial.Serial( + SERIAL_PORT, 115200, parity=serial.PARITY_EVEN, timeout=1 + ) as ser: + time.sleep(1) # Wait for NRSTs to clear from serial port establishment + + print(f"3. Beginning firmware flash") + + try: + flash_image(ser, image_path) + except RuntimeError as e: + if "Sync failed" in str(e): + raise RuntimeError("Ensure BOOT0 is raised, then retry") + + print("\tFirmware update successful") + + +def __nvm_reset(): + print(f"1. Opening serial port ({SERIAL_PORT})") + with serial.Serial(SERIAL_PORT, 115200) as ser: + time.sleep(1) # Wait for NRSTs to clear from serial port establishment + + print(f"2. Beginning NVM reset") + reset(ser) + + print("\tReset NVM") + + +def __nvm_memory_extract(): + print(f"1. Enter a starting address (Hex):") + section_start = input("> ").strip() + try: + section_start = int(section_start, 16) + except TypeError: + raise ValueError("Expected hexadecimal address") + + print(f"2. Enter a read length (recommended 4096):") + section_length = input("> ").strip() + try: + section_length = int(section_length) + except TypeError: + raise ValueError("Expected hexadecimal address") + + print(f"3. Enter a output filepath (recommended .txt):") + output_file_path = input("> ") + if len(output_file_path) < 5 or output_file_path[-4:] != ".txt": + output_file_path += ".txt" + + print(f"4. Opening serial port ({SERIAL_PORT})") + with serial.Serial(SERIAL_PORT, 115200) as ser: + # Pulse NRST before start + pulse_nrst(ser, duration_ms=50) + time.sleep(0.05) + + time.sleep(7) # Wait for NRSTs to clear from serial port establishment + + print(f"5. Beginning NVM read communication") + sector = read_section( + ser, start_addr=section_start, length=section_length + ) + + print(f"6. Beginning hex dump file save") + save_hexdump(sector, start_addr=section_start, filename=output_file_path) + + print(f"\tWrote {len(sector)} bytes to {output_file_path}") + + +def __serial_port_manual_config(): + global SERIAL_PORT + + print(f"Current serial port: {SERIAL_PORT}") + print("Enter a serial port:") + input_serial_port = input("> ").strip().lower() + if input_serial_port.isnumeric(): + SERIAL_PORT = f"COM{input_serial_port}" + else: + SERIAL_PORT = input_serial_port.upper() + print(f"\tSerial port configured to: {SERIAL_PORT}") + + +def __serial_port_auto_config(): + global SERIAL_PORT + + cp_ports = find_cp2102n_ports() + if cp_ports: + print( + f"\tFound CP2102N device(s): " + f"{', '.join([port['device'] for port in cp_ports])}" + ) + SERIAL_PORT = cp_ports[0]["device"] + print(f"\tSerial port configured to: {SERIAL_PORT}") + else: + print("\tNo CP2102N devices found, please add a serial port manually") + + +def header_print(): + print( + "-------------------------------------------------------------------------------\n" + " Momentum pyBlasher (v0.1.0-alpha) \n" + "-------------------------------------------------------------------------------\n" + ) + + +def end_of_command_print(): + print() + + +def main_menu_print(): + print( + " Options: (Not case sensitive)\n" + " 1 = Momentum firmware update\n" + " 2 = NVM reset (wipe memory)\n" + " 3 = NVM sector readout\n" + " 8 = Automatic serial port configuration\n" + " 9 = Manual serial port configuration\n" + " e = Exit\n" + ) + + +def run_cli(): + header_print() + + __serial_port_auto_config() + + end_of_command_print() + + try: + while True: + main_menu_print() + choice = input("> ").strip().lower()[0] + + start = time.time() + + try: + if choice == "1": + __flash_image() + elif choice == "2": + __nvm_reset() + elif choice == "3": + __nvm_memory_extract() + elif choice == "8": + __serial_port_auto_config() + elif choice == "9": + __serial_port_manual_config() + elif choice == "e": + raise KeyboardInterrupt + else: + print(f"Invalid choice: {choice!r}!") + except ValueError as e: + print(f"\tValueError: {e}") + except serial.serialutil.SerialException as e: + print(f"\tSerialException: {e}") + except FileNotFoundError as e: + print(f"\tFileNotFoundError: {e}") + except RuntimeError as e: + print(f"\tRuntimeError: {e}") + + print(f"\tCompleted in {time.time() - start} seconds") + + end_of_command_print() + + except KeyboardInterrupt: + print("\nTerminating program...") + exit(1) diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dbcc723c6d5cc67f82ff060b509b134489d24922 GIT binary patch literal 4753 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 JN^`H+{{q1m_VNG# literal 0 HcmV?d00001 diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..d9410e6 --- /dev/null +++ b/constants.py @@ -0,0 +1,5 @@ +"""pyBlasher constants.""" + +# Silicon Labs CP2102N default USB VID/PID. +CP2102N_VID = 0x10C4 +CP2102N_PID = 0xEA60 diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..2a3ae72 --- /dev/null +++ b/gui.py @@ -0,0 +1,205 @@ +"""pyBlasher GUI app.""" + +import time + +import serial +from kivy.app import App +from kivy.core.window import Window +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.filechooser import FileChooserListView +from kivy.uix.label import Label +from kivy.uix.popup import Popup +from kivy.uix.spinner import Spinner +from kivy.uix.textinput import TextInput +from kivy.uix.widget import Widget + +from flash_firmware import flash_image +from util import find_cp2102n_ports + +MSG_NO_PORTS_FOUND = "No ports found" + + +class FirmwareToolUI(BoxLayout): + def __init__(self, **kwargs): + super().__init__( + orientation="vertical", spacing=10, padding=10, **kwargs + ) + # Port selection + self.port_spinner = Spinner( + text="Click to select a port", + size_hint=(1, None), + height=40, + background_normal="", + background_color=(0.1, 0.4, 0.1, 1), + ) + self.add_widget(self.port_spinner) + self.add_widget( + Button( + text="Refresh ports", + size_hint=(1, None), + height=40, + background_normal="", + background_color=(0.1, 0.4, 0.1, 1), + on_press=lambda _: self.refresh_ports(), + ) + ) + + # Spacer + self.add_widget(Widget(size_hint_y=None, height=10)) + + # Firmware file selection + self.bin_label = Label( + text="No .bin selected", size_hint=(1, None), height=30 + ) + self.add_widget(self.bin_label) + self.add_widget( + Button( + text="Drop a .bin file here or click to browse", + size_hint=(1, None), + height=40, + background_normal="", + background_color=(0.8, 0.5, 0.1, 1), + on_press=self.browse_bin, + ) + ) + self.bin_path = None + Window.bind(on_drop_file=self._on_file_drop) + + # Spacer + self.add_widget(Widget(size_hint_y=None, height=10)) + + # Execute & Log + self.add_widget( + Button( + text="Flash Firmware", + size_hint=(1, None), + height=40, + background_normal="", + background_color=(0.8, 0.3, 0.3, 1), + on_press=lambda _: self.execute_flash(), + ) + ) + self.log_view = TextInput( + readonly=True, multiline=True, size_hint=(1, 1) + ) + self.add_widget(self.log_view) + + # Post init actions + self.refresh_ports() + + def log(self, message: str): + self.log_view.text += message + "\n" + + def refresh_ports(self): + found_ports = find_cp2102n_ports() + if found_ports: + self.port_spinner.values = found_ports + else: + self.port_spinner.values = [] + self.port_spinner.text = MSG_NO_PORTS_FOUND + log_text = ( + ",".join(self.port_spinner.values) + if self.port_spinner.values + else MSG_NO_PORTS_FOUND + ) + self.log(f"Ports refreshed: {log_text}") + + def browse_bin(self, _): + chooser = FileChooserListView(filters=["*.bin"]) + popup = Popup( + title="Select .bin file", content=chooser, size_hint=(0.9, 0.9) + ) + chooser.bind(selection=lambda fs, sel: self._select_bin(sel, popup)) + popup.open() + + def _select_bin(self, selection, popup): + if selection: + self.bin_path = selection[0] + self.bin_label.text = self.bin_path + popup.dismiss() + + def _on_file_drop(self, window, file_path, x, y): + path = file_path.decode("utf-8") + if path.endswith(".bin"): + self.bin_path = path + self.bin_label.text = path + self.log(f"Dropped .bin file: {path}") + else: + self.log(f"Ignored dropped file (not .bin): {path}") + + def _confirm_flash_proceed(self, port): + try: + ser = serial.Serial( + port, 115200, parity=serial.PARITY_EVEN, timeout=1 + ) + except Exception as e: + self.log(f"Could not open port {port}: {e}") + return + time.sleep(1) + self.log(f"Starting firmware update on {port} with {self.bin_path}") + try: + flash_image(ser, self.bin_path) + self.log("Firmware update successful.") + except Exception as e: + self.log(f"Error during flash: {e}") + finally: + ser.close() + + def execute_flash(self): + port = self.port_spinner.text + if port == MSG_NO_PORTS_FOUND: + self.log("Select a port!") + return + if not self.bin_path: + self.log("Select a .bin file!") + return + + confirm_layout = BoxLayout( + orientation="vertical", padding=10, spacing=10 + ) + confirm_layout.add_widget( + Label( + text=f"Proceed with flashing\n" + f"{self.bin_path}\n" + f"on port {port}?", + halign="center", + ) + ) + + button_row = BoxLayout(size_hint=(1, None), height=40, spacing=10) + yes_btn = Button(text="Yes", background_color=(0.1, 0.6, 0.1, 1)) + cancel_btn = Button(text="Cancel", background_color=(0.6, 0.1, 0.1, 1)) + button_row.add_widget(yes_btn) + button_row.add_widget(cancel_btn) + + confirm_layout.add_widget(button_row) + + popup = Popup( + title="Confirm Firmware Flash", + content=confirm_layout, + size_hint=(0.8, 0.6), + ) + + yes_btn.bind( + on_press=lambda _: ( + popup.dismiss(), + self._confirm_flash_proceed(port), + ) + ) + cancel_btn.bind(on_press=popup.dismiss) + + popup.open() + + +class PyBlasherApp(App): + def build(self): + Window.size = (600, 450) + Window.minimum_width = 350 + Window.minimum_height = 350 + Window.set_icon("assets/icon.png") + return FirmwareToolUI() + + +def run_gui(): + PyBlasherApp().run() diff --git a/main.py b/main.py index f645f5e..294393a 100644 --- a/main.py +++ b/main.py @@ -1,226 +1,15 @@ """Main pyBlasher application for Momentum.""" import sys -import time -import serial -from serial.tools import list_ports +from app import run_cli -from flash_firmware import flash_image, pulse_nrst -from nor_flash_comm import ( - reset, - read_section, - save_hexdump, -) - -# Silicon Labs CP2102N default USB VID/PID. -CP2102N_VID = 0x10C4 -CP2102N_PID = 0xEA60 - -SERIAL_PORT = "COM1" - - -def __flash_image(): - print(f"1. Enter a firmware filepath (.bin):") - image_path = input("> ") - if len(image_path) < 5 or image_path[-4:] != ".bin": - image_path += ".bin" - - print(f"2. Opening serial port ({SERIAL_PORT})") - with serial.Serial( - SERIAL_PORT, 115200, parity=serial.PARITY_EVEN, timeout=1 - ) as ser: - time.sleep(1) # Wait for NRSTs to clear from serial port establishment - - print(f"3. Beginning firmware flash") - - try: - flash_image(ser, image_path) - except RuntimeError as e: - if "Sync failed" in str(e): - raise RuntimeError("Ensure BOOT0 is raised, then retry") - - print("\tFirmware update successful") - - -def __nvm_reset(): - print(f"1. Opening serial port ({SERIAL_PORT})") - with serial.Serial(SERIAL_PORT, 115200) as ser: - time.sleep(1) # Wait for NRSTs to clear from serial port establishment - - print(f"2. Beginning NVM reset") - reset(ser) - - print("\tReset NVM") - - -def __nvm_memory_extract(): - print(f"1. Enter a starting address (Hex):") - section_start = input("> ").strip() - try: - section_start = int(section_start, 16) - except TypeError: - raise ValueError("Expected hexadecimal address") - - print(f"2. Enter a read length (recommended 4096):") - section_length = input("> ").strip() - try: - section_length = int(section_length) - except TypeError: - raise ValueError("Expected hexadecimal address") - - print(f"3. Enter a output filepath (recommended .txt):") - output_file_path = input("> ") - if len(output_file_path) < 5 or output_file_path[-4:] != ".txt": - output_file_path += ".txt" - - print(f"4. Opening serial port ({SERIAL_PORT})") - with serial.Serial(SERIAL_PORT, 115200) as ser: - # Pulse NRST before start - pulse_nrst(ser, duration_ms=50) - time.sleep(0.05) - - time.sleep(7) # Wait for NRSTs to clear from serial port establishment - - print(f"5. Beginning NVM read communication") - sector = read_section( - ser, start_addr=section_start, length=section_length - ) - - print(f"6. Beginning hex dump file save") - save_hexdump(sector, start_addr=section_start, filename=output_file_path) - - print(f"\tWrote {len(sector)} bytes to {output_file_path}") - - -def __serial_port_manual_config(): - global SERIAL_PORT - - print(f"Current serial port: {SERIAL_PORT}") - print("Enter a serial port:") - input_serial_port = input("> ").strip().lower() - if input_serial_port.isnumeric(): - SERIAL_PORT = f"COM{input_serial_port}" - else: - SERIAL_PORT = input_serial_port.upper() - print(f"\tSerial port configured to: {SERIAL_PORT}") - - -def __find_cp2102n_ports(): - """Scan serial ports and return those matching the CP2102N VID/PID.""" - matches = [] - # Pre-build the hex string once for fallback matching - vid_pid = f"{CP2102N_VID:04X}:{CP2102N_PID:04X}".lower() - - for port in list_ports.comports(): - # Primary check via explicit attributes (pyserial 3.4) - if port.vid == CP2102N_VID and port.pid == CP2102N_PID: - matches.append( - { - "device": port.device, - "description": port.description, - "hwid": port.hwid, - } - ) - - # Fallback: case-insensitive search in the hwid string - elif port.hwid and vid_pid in port.hwid.lower(): - matches.append( - { - "device": port.device, - "description": port.description, - "hwid": port.hwid, - } - ) - - return matches - - -def __serial_port_auto_config(): - global SERIAL_PORT - - cp_ports = __find_cp2102n_ports() - if cp_ports: - print( - f"\tFound CP2102N device(s): " - f"{', '.join([port['device'] for port in cp_ports])}" - ) - SERIAL_PORT = cp_ports[0]["device"] - print(f"\tSerial port configured to: {SERIAL_PORT}") +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] in ("-c", "--cli"): + # Run CLI app + run_cli() else: - print("\tNo CP2102N devices found, please add a serial port manually") - - -def header_print(): - print( - "-------------------------------------------------------------------------------\n" - " Momentum pyBlasher (v0.1.0-alpha) \n" - "-------------------------------------------------------------------------------\n" - ) - - -def end_of_command_print(): - print() - + # Run GUI app + from gui import run_gui -def main_menu_print(): - print( - " Options: (Not case sensitive)\n" - " 1 = Momentum firmware update\n" - " 2 = NVM reset (wipe memory)\n" - " 3 = NVM sector readout\n" - " 8 = Automatic serial port configuration\n" - " 9 = Manual serial port configuration\n" - " e = Exit\n" - ) - - -def main(): - header_print() - - __serial_port_auto_config() - - end_of_command_print() - - try: - while True: - main_menu_print() - choice = input("> ").strip().lower()[0] - - start = time.time() - - try: - if choice == "1": - __flash_image() - elif choice == "2": - __nvm_reset() - elif choice == "3": - __nvm_memory_extract() - elif choice == "8": - __serial_port_auto_config() - elif choice == "9": - __serial_port_manual_config() - elif choice == "e": - raise KeyboardInterrupt - else: - print(f"Invalid choice: {choice!r}!") - except ValueError as e: - print(f"\tValueError: {e}") - except serial.serialutil.SerialException as e: - print(f"\tSerialException: {e}") - except FileNotFoundError as e: - print(f"\tFileNotFoundError: {e}") - except RuntimeError as e: - print(f"\tRuntimeError: {e}") - - print(f"\tCompleted in {time.time() - start} seconds") - - end_of_command_print() - - except KeyboardInterrupt: - print("\nTerminating program...") - sys.exit(1) - - -if __name__ == "__main__": - main() + run_gui() diff --git a/requirements.txt b/requirements.txt index cb8f9a2..2b2238f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ # $ pip install -r requirements.txt +Kivy~=2.3.1 pyserial~=3.5 diff --git a/util.py b/util.py new file mode 100644 index 0000000..8197ffe --- /dev/null +++ b/util.py @@ -0,0 +1,17 @@ +"""pyBlasher utility helper functions.""" + +from serial.tools import list_ports + +from constants import * + + +def find_cp2102n_ports(): + """Scan serial ports and return those matching the CP2102N VID/PID.""" + matches = [] + vid_pid = f"{CP2102N_VID:04X}:{CP2102N_PID:04X}".lower() + for port in list_ports.comports(): + if port.vid == CP2102N_VID and port.pid == CP2102N_PID: + matches.append(port.device) + elif port.hwid and vid_pid in port.hwid.lower(): + matches.append(port.device) + return matches From 03da2d4f02625ef3b8b26ec502b3fe89238e24a0 Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 02:56:22 -0400 Subject: [PATCH 02/11] Attempt pyinstaller workflow error fix --- .github/workflows/pyinstaller.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index 2613350..bc4ebf9 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -38,7 +38,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.x" + python-version: "3.10" - name: Install dependencies run: | From 1e4b2dfeffad136c7ea6224a4e3a2b92cbf64eb1 Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:04:13 -0400 Subject: [PATCH 03/11] Revert workflow edits, disable mtdev --- .github/workflows/pyinstaller.yaml | 2 +- gui.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index bc4ebf9..2613350 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -38,7 +38,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.x" - name: Install dependencies run: | diff --git a/gui.py b/gui.py index 2a3ae72..39f07ba 100644 --- a/gui.py +++ b/gui.py @@ -4,6 +4,7 @@ import serial from kivy.app import App +from kivy.config import Config from kivy.core.window import Window from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button @@ -17,6 +18,9 @@ from flash_firmware import flash_image from util import find_cp2102n_ports +# Disable mtdev (multi touch) to resolve executable builds on Linux +Config.set("input", "mtdev", "") + MSG_NO_PORTS_FOUND = "No ports found" From 2e14b157d99330e214190d93ebda09acef1fa959 Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:07:35 -0400 Subject: [PATCH 04/11] Fix import and mtdev disable order --- gui.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gui.py b/gui.py index 39f07ba..606b6c1 100644 --- a/gui.py +++ b/gui.py @@ -1,10 +1,15 @@ """pyBlasher GUI app.""" +# Disable mtdev (multi touch) to resolve executable builds on Linux +from kivy.config import Config + +Config.set("input", "mtdev", "") + +# Usual imports import time import serial from kivy.app import App -from kivy.config import Config from kivy.core.window import Window from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button @@ -18,9 +23,6 @@ from flash_firmware import flash_image from util import find_cp2102n_ports -# Disable mtdev (multi touch) to resolve executable builds on Linux -Config.set("input", "mtdev", "") - MSG_NO_PORTS_FOUND = "No ports found" From ea09da20ab9d0f07743fc47176e82cd0128250fe Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:13:14 -0400 Subject: [PATCH 05/11] Implement mtdev install in workflow --- .github/workflows/pyinstaller.yaml | 7 +++++++ gui.py | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index 2613350..554f512 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -35,6 +35,13 @@ jobs: with: submodules: true + # Required for use of Kivy + - name: Install MTDev on Linux + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libmtdev-dev libmtdev1 + - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/gui.py b/gui.py index 606b6c1..2a3ae72 100644 --- a/gui.py +++ b/gui.py @@ -1,11 +1,5 @@ """pyBlasher GUI app.""" -# Disable mtdev (multi touch) to resolve executable builds on Linux -from kivy.config import Config - -Config.set("input", "mtdev", "") - -# Usual imports import time import serial From bb0e8df5b683e979ca5eae2911116f28996051d8 Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:18:34 -0400 Subject: [PATCH 06/11] Ensure Kivy dependencies on Linux --- .github/workflows/pyinstaller.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index 554f512..b0a0b79 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -40,7 +40,9 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libmtdev-dev libmtdev1 + sudo apt-get install -y \ + libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev \ + libgl1-mesa-dev libmtdev-dev libmtdev1 - name: Set up Python uses: actions/setup-python@v5 From c4e1852a1ade1eaacfcbbe5c76f94f7ca47ea184 Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:27:34 -0400 Subject: [PATCH 07/11] Ensure Kivy dependencies on Windows --- .github/workflows/pyinstaller.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index b0a0b79..b88cd49 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -43,6 +43,10 @@ jobs: sudo apt-get install -y \ libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev \ libgl1-mesa-dev libmtdev-dev libmtdev1 + - name: Install Kivy SDL2 & GLEW + if: matrix.os == 'windows-latest' + run: | + pip install kivy_deps.sdl2 kivy_deps.glew - name: Set up Python uses: actions/setup-python@v5 From ae1f95d92803c51cc221f861e603492257019736 Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:31:23 -0400 Subject: [PATCH 08/11] Fix install order in workflow --- .github/workflows/pyinstaller.yaml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index b88cd49..d01bcab 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -35,24 +35,26 @@ jobs: with: submodules: true - # Required for use of Kivy - - name: Install MTDev on Linux + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + # Kivy system deps on Linux + - name: Install system libs (Linux) if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y \ libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev \ libgl1-mesa-dev libmtdev-dev libmtdev1 - - name: Install Kivy SDL2 & GLEW + + # Kivy SDL2 & GLEW on Windows + - name: Install Kivy SDL2 & GLEW (Windows) if: matrix.os == 'windows-latest' run: | pip install kivy_deps.sdl2 kivy_deps.glew - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install dependencies run: | python -m pip install --upgrade pip From 21c70371a57e8387f7b1125d9fc9f120b3e92edc Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:34:16 -0400 Subject: [PATCH 09/11] Attempt fix for windows pyinstaller workflow --- .github/workflows/pyinstaller.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index d01bcab..e0e94d9 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -50,10 +50,10 @@ jobs: libgl1-mesa-dev libmtdev-dev libmtdev1 # Kivy SDL2 & GLEW on Windows - - name: Install Kivy SDL2 & GLEW (Windows) + - name: Install Kivy SDL2, GLEW & ANGLE (Windows) if: matrix.os == 'windows-latest' run: | - pip install kivy_deps.sdl2 kivy_deps.glew + pip install kivy_deps.sdl2 kivy_deps.glew kivy_deps.angle - name: Install dependencies run: | From 25d21ba2cb79aae5d383628f41dbe9ac4b342a12 Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:38:40 -0400 Subject: [PATCH 10/11] Attempt fix for windows (again) --- .github/workflows/pyinstaller.yaml | 6 ------ gui.py | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index e0e94d9..71d4cb3 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -49,12 +49,6 @@ jobs: libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev \ libgl1-mesa-dev libmtdev-dev libmtdev1 - # Kivy SDL2 & GLEW on Windows - - name: Install Kivy SDL2, GLEW & ANGLE (Windows) - if: matrix.os == 'windows-latest' - run: | - pip install kivy_deps.sdl2 kivy_deps.glew kivy_deps.angle - - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/gui.py b/gui.py index 2a3ae72..30456ee 100644 --- a/gui.py +++ b/gui.py @@ -1,5 +1,14 @@ """pyBlasher GUI app.""" +import os + +from kivy.config import Config + +# If building via GitHub Actions, force the mock window (no real GL) +if os.environ.get("GITHUB_ACTIONS") == "true": + Config.set("kivy", "window", "mock") + Config.set("input", "mtdev", "") + import time import serial From b4d30b37cce848436c5d23e745b05980325263a3 Mon Sep 17 00:00:00 2001 From: danielljeon <48095779+danielljeon@users.noreply.github.com> Date: Sat, 28 Jun 2025 03:48:26 -0400 Subject: [PATCH 11/11] Nuke pyinstaller workflow, relocate to docs --- {.github/workflows => docs}/pyinstaller.yaml | 0 gui.py | 9 --------- 2 files changed, 9 deletions(-) rename {.github/workflows => docs}/pyinstaller.yaml (100%) diff --git a/.github/workflows/pyinstaller.yaml b/docs/pyinstaller.yaml similarity index 100% rename from .github/workflows/pyinstaller.yaml rename to docs/pyinstaller.yaml diff --git a/gui.py b/gui.py index 30456ee..2a3ae72 100644 --- a/gui.py +++ b/gui.py @@ -1,14 +1,5 @@ """pyBlasher GUI app.""" -import os - -from kivy.config import Config - -# If building via GitHub Actions, force the mock window (no real GL) -if os.environ.get("GITHUB_ACTIONS") == "true": - Config.set("kivy", "window", "mock") - Config.set("input", "mtdev", "") - import time import serial