Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 84 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: CI
on:
push:
branches: ["**"]
tags:
- "v*"
- "V*"
pull_request:
branches: ["**"]

Expand All @@ -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
Expand All @@ -46,15 +62,74 @@ 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
with:
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
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
File renamed without changes.
File renamed without changes.
83 changes: 76 additions & 7 deletions stm32_easy_flash.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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...")

Expand Down Expand Up @@ -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.
Expand Down
Loading