diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef77d98..120a00b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,9 @@ name: CI on: push: branches: ["**"] + tags: + - "v*" + - "V*" pull_request: branches: ["**"] @@ -20,17 +23,30 @@ jobs: with: python-version: "3.11" - - name: Install linting tools - run: pip install flake8 pylint + - 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: Install project dependencies - run: pip install -r requirements.txt + - 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 @@ -46,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 @@ -58,3 +89,47 @@ 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') || startsWith(github.ref, 'refs/tags/V') + permissions: + contents: write + + steps: + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: stm32-easy-flash-windows + path: dist + + - 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. + + ### Configure + Place `stm32_easy_flash.config.json` next to the executable and set: + - `CLI_PATH` + - `FIRMWARE_PATH` + - `PORT` + - optional: `HOTKEY` + + ### Usage + 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 + - ST-LINK or compatible debug adapter connected + draft: false + prerelease: false + files: dist/stm32_easy_flash.exe 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 new file mode 100644 index 0000000..9d6acec --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "stm32-easy-flash" +version = "1.0.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/mootseeker/stm32-easy-flash" +Issues = "https://github.com/mootseeker/stm32-easy-flash/issues" + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.backends.legacy:build" 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.