From 612767ea45f5513b6eb2471a32ba4ccdd4032427 Mon Sep 17 00:00:00 2001 From: Kevin Perillo Date: Mon, 16 Mar 2026 15:28:08 +0100 Subject: [PATCH 1/4] feat: add GitHub Release workflow and pyproject.toml - CI workflow: new 'release' job triggered only on v* tags - Runs after 'build' job passes (lint -> build -> release) - Builds .exe with PyInstaller - Creates GitHub Release via softprops/action-gh-release@v2 - Attaches stm32_easy_flash.exe as download artifact - Release notes include download instructions and requirements - Added tag trigger (v*) to CI push trigger - pyproject.toml: project metadata for version tracking To create a release: git tag v1.0.0 git push origin v1.0.0 --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 18 +++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef77d98..658e076 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,8 @@ name: CI on: push: branches: ["**"] + tags: + - "v*" pull_request: branches: ["**"] @@ -58,3 +60,48 @@ jobs: name: stm32-easy-flash-windows path: dist/stm32_easy_flash.exe retention-days: 30 + + release: + name: Create GitHub Release + runs-on: windows-latest + needs: build + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install build dependencies + run: pip install -r requirements-build.txt + + - name: Build .exe with PyInstaller + run: pyinstaller stm32_easy_flash.spec + + - name: Create GitHub Release and upload .exe + uses: softprops/action-gh-release@v2 + with: + name: "STM32 Easy Flash ${{ github.ref_name }}" + body: | + ## STM32 Easy Flash ${{ github.ref_name }} + + ### Download + Download `stm32_easy_flash.exe` below and run it directly — no Python installation required. + + ### Usage + 1. Edit `CLI_PATH`, `FIRMWARE_PATH` and `PORT` in the source **or** use the pre-built binary with defaults + 2. Run `stm32_easy_flash.exe` + 3. Press **Ctrl + Shift + F12** to flash your STM32 + + ### Requirements + - STM32CubeProgrammer installed at the configured path + - ST-LINK or compatible debug adapter connected + draft: false + prerelease: false + files: dist/stm32_easy_flash.exe diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a71e06 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "stm32-easy-flash" +version = "0.1.0" +description = "Hotkey-triggered firmware flashing for STM32 microcontrollers via STM32CubeProgrammer CLI" +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.9" +dependencies = [ + "keyboard", +] + +[project.urls] +Homepage = "https://github.com/YOUR_USERNAME/stm32-easy-flash" +Issues = "https://github.com/YOUR_USERNAME/stm32-easy-flash/issues" + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.backends.legacy:build" From 1afe16be168a4f0a0055974924d95a183b973648 Mon Sep 17 00:00:00 2001 From: Kevin Perillo Date: Mon, 16 Mar 2026 16:11:14 +0100 Subject: [PATCH 2/4] fix: update project URLs in pyproject.toml to reflect correct repository --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8a71e06..77650c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,8 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/YOUR_USERNAME/stm32-easy-flash" -Issues = "https://github.com/YOUR_USERNAME/stm32-easy-flash/issues" +Homepage = "https://github.com/mootseeker/stm32-easy-flash" +Issues = "https://github.com/mootseeker/stm32-easy-flash/issues" [build-system] requires = ["setuptools>=68"] From 796e8cd891cc4c93b132636ae866959c8776bbbc Mon Sep 17 00:00:00 2001 From: Kevin Perillo Date: Mon, 16 Mar 2026 18:30:41 +0100 Subject: [PATCH 3/4] feat: harden release flow and add runtime config - support both v* and V* tag releases in CI - publish downloaded build artifact in release job (no rebuild) - add JSON/env runtime configuration and startup validation - document executable configuration in README - reorganize dependency files under requirements/ - remove obsolete root requirements files --- .github/workflows/ci.yml | 38 ++++----- README.md | 29 ++++--- pyproject.toml | 2 +- .../build.txt | 0 requirements.txt => requirements/runtime.txt | 0 stm32_easy_flash.py | 83 +++++++++++++++++-- 6 files changed, 115 insertions(+), 37 deletions(-) rename requirements-build.txt => requirements/build.txt (100%) rename requirements.txt => requirements/runtime.txt (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 658e076..f1769fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: ["**"] tags: - "v*" + - "V*" pull_request: branches: ["**"] @@ -26,7 +27,7 @@ jobs: run: pip install flake8 pylint - name: Install project dependencies - run: pip install -r requirements.txt + run: pip install -r requirements/runtime.txt - name: Run flake8 (style & syntax check) run: flake8 stm32_easy_flash.py @@ -49,7 +50,7 @@ jobs: python-version: "3.11" - name: Install build dependencies - run: pip install -r requirements-build.txt + run: pip install -r requirements/build.txt - name: Build .exe with PyInstaller run: pyinstaller stm32_easy_flash.spec @@ -65,24 +66,16 @@ jobs: name: Create GitHub Release runs-on: windows-latest needs: build - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/V') permissions: contents: write steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + - name: Download build artifact + uses: actions/download-artifact@v4 with: - python-version: "3.11" - - - name: Install build dependencies - run: pip install -r requirements-build.txt - - - name: Build .exe with PyInstaller - run: pyinstaller stm32_easy_flash.spec + name: stm32-easy-flash-windows + path: dist - name: Create GitHub Release and upload .exe uses: softprops/action-gh-release@v2 @@ -92,12 +85,19 @@ jobs: ## STM32 Easy Flash ${{ github.ref_name }} ### Download - Download `stm32_easy_flash.exe` below and run it directly — no Python installation required. + Download `stm32_easy_flash.exe` below and run it directly - no Python installation required. + + ### Configure + Place `stm32_easy_flash.config.json` next to the executable and set: + - `CLI_PATH` + - `FIRMWARE_PATH` + - `PORT` + - optional: `HOTKEY` ### Usage - 1. Edit `CLI_PATH`, `FIRMWARE_PATH` and `PORT` in the source **or** use the pre-built binary with defaults - 2. Run `stm32_easy_flash.exe` - 3. Press **Ctrl + Shift + F12** to flash your STM32 + 1. Run `stm32_easy_flash.exe` + 2. Press your configured hotkey (default: `Ctrl+Shift+F12`) + 3. Flashing starts automatically ### Requirements - STM32CubeProgrammer installed at the configured path diff --git a/README.md b/README.md index 8a80b70..43e6c2a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Listens for a global hotkey (`Ctrl+Shift+F12`) and automatically flashes an STM3 Install the dependency with: ```bash -pip install keyboard +pip install -r requirements/runtime.txt ``` > **Note:** On Windows the script must be run with **administrator privileges** for the global hotkey listener to work reliably. @@ -43,19 +43,28 @@ pip install keyboard ## Configuration -Open `stm32_easy_flash.py` and adjust the three constants at the top of the file: +The tool supports runtime config from a JSON file placed next to the script/exe: -```python -# Path to STM32 Programmer CLI (default path on Windows) -CLI_PATH = r"C:\ST\STM32CubeProgrammer\bin\STM32_Programmer_CLI.exe" +- `stm32_easy_flash.config.json` -# Path to your firmware file (.hex, .bin, .elf, …) -FIRMWARE_PATH = r"C:\path\to\your\firmware.hex" +Create `stm32_easy_flash.config.json` with the following content: -# Connection type: "SWD" for ST-LINK | "USB1" for DFU | "COM3" for UART -PORT = "SWD" +```json +{ + "CLI_PATH": "C:\\ST\\STM32CubeProgrammer\\bin\\STM32_Programmer_CLI.exe", + "FIRMWARE_PATH": "C:\\path\\to\\your\\firmware.hex", + "PORT": "SWD", + "HOTKEY": "ctrl+shift+f12" +} ``` +Optional environment variables (override JSON values): + +- `STM32_EASY_FLASH_CLI_PATH` +- `STM32_EASY_FLASH_FIRMWARE_PATH` +- `STM32_EASY_FLASH_PORT` +- `STM32_EASY_FLASH_HOTKEY` + --- ## Usage @@ -64,7 +73,7 @@ PORT = "SWD" python stm32_easy_flash.py ``` -The script starts and waits in the background. Whenever you press **Ctrl+Shift+F12**, it: +The script/exe starts and waits in the background. Whenever you press your configured hotkey (default: **Ctrl+Shift+F12**), it: 1. Erases the entire chip 2. Writes the configured firmware diff --git a/pyproject.toml b/pyproject.toml index 77650c5..9d6acec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stm32-easy-flash" -version = "0.1.0" +version = "1.0.0" description = "Hotkey-triggered firmware flashing for STM32 microcontrollers via STM32CubeProgrammer CLI" readme = "README.md" license = { text = "MIT" } diff --git a/requirements-build.txt b/requirements/build.txt similarity index 100% rename from requirements-build.txt rename to requirements/build.txt diff --git a/requirements.txt b/requirements/runtime.txt similarity index 100% rename from requirements.txt rename to requirements/runtime.txt diff --git a/stm32_easy_flash.py b/stm32_easy_flash.py index b318b16..d8c981e 100644 --- a/stm32_easy_flash.py +++ b/stm32_easy_flash.py @@ -1,8 +1,11 @@ """STM32 Easy Flash - Hotkey-triggered firmware flashing via STM32CubeProgrammer CLI.""" +import json +import os import subprocess import sys import time +from pathlib import Path import keyboard @@ -11,19 +14,81 @@ # ========================================== # Path to STM32 Programmer CLI (default path on Windows) -CLI_PATH = r"C:\ST\STM32CubeProgrammer\bin\STM32_Programmer_CLI.exe" +DEFAULT_CLI_PATH = r"C:\ST\STM32CubeProgrammer\bin\STM32_Programmer_CLI.exe" # Path to your firmware file -FIRMWARE_PATH = r"C:\path\to\your\firmware.hex" +DEFAULT_FIRMWARE_PATH = r"C:\path\to\your\firmware.hex" # Connection type (e.g. "SWD" for ST-LINK, "USB1" for DFU, "COM3" for UART) -PORT = "SWD" +DEFAULT_PORT = "SWD" + +# Global hotkey to trigger flashing +DEFAULT_HOTKEY = "ctrl+shift+f12" + +CONFIG_FILENAME = "stm32_easy_flash.config.json" + +CLI_PATH = DEFAULT_CLI_PATH +FIRMWARE_PATH = DEFAULT_FIRMWARE_PATH +PORT = DEFAULT_PORT +HOTKEY = DEFAULT_HOTKEY # ========================================== +def _runtime_directory(): + """Return the directory where config should be loaded from.""" + if getattr(sys, "frozen", False): + return Path(sys.executable).resolve().parent + return Path(__file__).resolve().parent + + +def load_runtime_config(): + """Load runtime configuration from JSON file and environment variables.""" + global CLI_PATH, FIRMWARE_PATH, PORT, HOTKEY + + config_path = _runtime_directory() / CONFIG_FILENAME + file_config = {} + + if config_path.exists(): + try: + with config_path.open("r", encoding="utf-8") as file_handle: + loaded = json.load(file_handle) + if isinstance(loaded, dict): + file_config = loaded + else: + print("Warning: Config file does not contain a JSON object. Using defaults.") + except (OSError, json.JSONDecodeError) as error: + print(f"Warning: Could not read config file '{config_path}': {error}") + + CLI_PATH = os.getenv("STM32_EASY_FLASH_CLI_PATH", file_config.get("CLI_PATH", DEFAULT_CLI_PATH)) + FIRMWARE_PATH = os.getenv("STM32_EASY_FLASH_FIRMWARE_PATH", file_config.get("FIRMWARE_PATH", DEFAULT_FIRMWARE_PATH)) + PORT = os.getenv("STM32_EASY_FLASH_PORT", file_config.get("PORT", DEFAULT_PORT)) + HOTKEY = os.getenv("STM32_EASY_FLASH_HOTKEY", file_config.get("HOTKEY", DEFAULT_HOTKEY)) + + return config_path + + +def validate_configuration(): + """Validate required runtime settings before attempting to flash.""" + if not Path(CLI_PATH).is_file(): + print(f"\nError: CLI executable not found at '{CLI_PATH}'.") + print("Update CLI_PATH in config/env before flashing.") + return False + + if not Path(FIRMWARE_PATH).is_file(): + print(f"\nError: Firmware file not found at '{FIRMWARE_PATH}'.") + print("Update FIRMWARE_PATH in config/env before flashing.") + return False + + return True + + def flash_mcu(): """Erase and flash the STM32 target using STM32_Programmer_CLI.""" + if not validate_configuration(): + print("Waiting for a valid configuration...") + return + print("\n" + "-"*40) print("▶ Hotkey detected! Starting flash process...") @@ -61,19 +126,23 @@ def flash_mcu(): print(f"\n❌ An unexpected error occurred: {e}") print("-" * 40) - print("Waiting for 'ctrl + shift + f12'... (Exit with Ctrl+C)") + print(f"Waiting for '{HOTKEY}'... (Exit with Ctrl+C)") # Main program if __name__ == "__main__": + config_file_path = load_runtime_config() + print("STM32 Easy Flash started.") + print(f"Config file: {config_file_path}") + print(f"Configured hotkey: {HOTKEY}") print(f"Configured port: {PORT}") print(f"Firmware: {FIRMWARE_PATH}") - print("\nWaiting for 'ctrl + shift + f12'... (Exit with Ctrl+C in terminal)") + print(f"\nWaiting for '{HOTKEY}'... (Exit with Ctrl+C in terminal)") try: - # Register hotkey. Pressing ctrl+shift+f12 will trigger flash_mcu() - keyboard.add_hotkey('ctrl+shift+f12', flash_mcu) + # Register hotkey from config/env; default remains ctrl+shift+f12. + keyboard.add_hotkey(HOTKEY, flash_mcu) # Keep the script alive so the hotkey listener remains active. # Press Ctrl+C in the terminal to exit cleanly. From 5895cea97239d9a038fd48a0c6397e23af17bb4a Mon Sep 17 00:00:00 2001 From: Kevin Perillo Date: Mon, 16 Mar 2026 18:38:22 +0100 Subject: [PATCH 4/4] feat: enhance CI workflow with caching for linting and build environments --- .github/workflows/ci.yml | 48 +++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1769fe..120a00b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,17 +23,30 @@ jobs: with: python-version: "3.11" - - name: Install linting tools - run: pip install flake8 pylint - - - name: Install project dependencies - run: pip install -r requirements/runtime.txt + - name: Restore lint environment cache + id: lint-cache + uses: actions/cache@v4 + with: + path: .venv-lint + key: ${{ runner.os }}-py311-lint-${{ hashFiles('requirements/runtime.txt') }} + restore-keys: | + ${{ runner.os }}-py311-lint- + + - name: Create lint environment and install dependencies + if: steps.lint-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + python -m venv .venv-lint + .\.venv-lint\Scripts\python.exe -m pip install --upgrade pip + .\.venv-lint\Scripts\python.exe -m pip install -r requirements/runtime.txt flake8 pylint - name: Run flake8 (style & syntax check) - run: flake8 stm32_easy_flash.py + shell: pwsh + run: .\.venv-lint\Scripts\python.exe -m flake8 stm32_easy_flash.py - name: Run pylint (code quality check) - run: pylint stm32_easy_flash.py --fail-under=7.0 + shell: pwsh + run: .\.venv-lint\Scripts\python.exe -m pylint stm32_easy_flash.py --fail-under=7.0 build: name: Build Executable @@ -49,11 +62,26 @@ jobs: with: python-version: "3.11" - - name: Install build dependencies - run: pip install -r requirements/build.txt + - name: Restore build environment cache + id: build-cache + uses: actions/cache@v4 + with: + path: .venv-build + key: ${{ runner.os }}-py311-build-${{ hashFiles('requirements/build.txt', 'stm32_easy_flash.spec') }} + restore-keys: | + ${{ runner.os }}-py311-build- + + - name: Create build environment and install dependencies + if: steps.build-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + python -m venv .venv-build + .\.venv-build\Scripts\python.exe -m pip install --upgrade pip + .\.venv-build\Scripts\python.exe -m pip install -r requirements/build.txt - name: Build .exe with PyInstaller - run: pyinstaller stm32_easy_flash.spec + shell: pwsh + run: .\.venv-build\Scripts\python.exe -m PyInstaller stm32_easy_flash.spec - name: Upload artifact uses: actions/upload-artifact@v4