diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index 8b08d6d..0000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,156 +0,0 @@
-stm32pio changelog:
-
- ver. 0.1 (30.11.17):
- - Initial version
-
- ver. 0.2 (14.01.18):
- - New: this changelog and more comments :)
- - Fixed: compatible with new filename politics (see PlatformIO issue #1107)
- ('inc' now must be 'include' so we add option to 'platformio.ini')
- - Changed: use os.path.normpath() instead of manually removing trailing '/'
-
- ver. 0.21 (18.01.18):
- - New: checking board name before PlatformIO start
-
- ver. 0.4 (03-04.04.18):
- - New: hide CubeMX and PlatformIO stdout output
- - New: shebang
- - New: choose your favourite editor with '--start-editor' option (replaces '--with-atom')
- - New: logging module
- - New: more checks
- - New: 'settings.py' file
- - New: cross-platform running
- - New: debug output (verbose '-v' mode)
- - New: 'README.md' and more comments
- - Fixed: remove unnecessary imports
- - Fixed: command to initialize PlatformIO project (remove double quotation marks)
- - Changed: many architectural improvements
- - Changed: documentation improvements
-
- ver. 0.45 (04-05.04.18):
- - New: introducing unit-tests for the app
- - New: clean-up feature
-
- ver. 0.5 (07.04.18):
- - New: more comments
- - New: screenshots for the usage example
- - Fixed: many small fixes and improvements
- - Changed: test now is more isolated and uses ./stm32pio-test/stm32pio-test.ioc file
-
- ver. 0.7 (05-07.11.18):
- - New: Windows support!
- - New: new editors support (Sublime Text)
- - New: more comments and docstrings
- - New: more checks to improve robustness
- - New: if __name__ == '__main__' block
- - New: new test: build generated project
- - New: new test: run editors
- - New: new test: user's code preservation after the code regeneration
- - New: clean run for test cases (implemented using decorator)
- - Fixed: compatible with latest PlatformIO project structure (ver 3.6.1)
- - Fixed: many small fixes and improvements
- - Changed: 'java_cmd' parameter in 'settings.py' (simple 'java' by default)
- - Changed: move to double-quoted strings
- - Changed: remove '_getProjectNameByPath()' function (replaced by 'os.path.basename()')
- - Changed: vast f-strings usage
- - Changed: test '.ioc' file is updated to the latest STM32CubeMX version (4.27.0 at the moment)
- - Changed: use 'os.path.join()' instead of manually composing of paths
- - Changed: use 'with ... as ...' construction for opening files
- - Changed: 120 chars line width
- - Changed: PEP 8 conformity: variables and functions naming conventions
- - Changed: PEP 8 conformity: multi-line imports
- - Changed: 'miscs.py' module is renamed to 'util.py'
-
- ver. 0.73 (10-11.02.19):
- - New: use more convenient Python project structure
- - New: package can be install using setuptools
- - New: TO-DO list
- - New: '--directory' option is now optional if the program gets called from the project directory
- - Fixed: license copyright
- - Fixed: 'dot' path will be handle successfully now
- - Fixed: bug on case insensitive machines
- - Fixed: bug in tests that allowing to pass the test even in failure situation
- - Changed: test '.ioc' file is updated to the latest STM32CubeMX version (5.0.1 at the moment)
- - Changed: documentation improvements
-
- ver. 0.74 (27.02.19):
- - New: new internal _get_project_path() function (more clean main script)
- - New: optional '--with-build' option for 'new' mode allowing to make an initial build to save a time
- - Changed: util.py functions now raising the exceptions instead of forcing the exit
- - Changed: test '.ioc' file is updated to the latest STM32CubeMX version (5.1.0 at the moment)
- - Changed: documentation improvements
-
- ver. 0.8 (09.19):
- - New: setup.py can now install executable script to run 'stm32pio' from any location
- - New: stm32pio logo/schematic
- - New: add PyCharm to .gitignore
- - New: add clear TODOs for the next release (some sort of a roadmap)
- - New: single __version__ reference
- - New: extended shebang
- - New: add some new tests (test_build_should_raise, test_file_not_found)
- - Fixed: options '--start-editor' and '--with-build' can now be used both for 'new' and 'generate' commands
- - Fixed: import scheme is now as it should be
- - Changed: migrate from 'os.path' to 'pathlib' as much as possible for paths management (as a more high-level module)
- - Changed: 'start editor' feature is now starting an arbitrary editor (in the same way as you do it from the terminal)
- - Changed: take outside 'platformio' command (to 'settings')
- - Changed: screenshots were actualized for recent CubeMX versions
- - Changed: logging output in standard (non-verbose) mode is simpler
- - Changed: move tests in new location
- - Changed: revised and improved tests
- - Changed: actualized .ioc file and clean-up the code according to the latest STM32CubeMX version (5.3.0 at the moment)
- - Changed: revised and improved util module
-
- ver. 0.9 (11-12.19):
- - New: tested with Python3 version of PlatformIO
- - New: '__main__.py' file (to run the app as module (python -m stm32pio))
- - New: 'init' subcommand (initialize the project only, useful for the preliminary tweaking)
- - New: introducing the OOP pattern: we have now a Stm32pio class representing a single project (project path as a main identifier)
- - New: projects now have a config file stm32pio.ini where the user can set the variety of parameters
- - New: 'state' property calculating the estimated project state on every request to itself (beta). It is the concept for future releases
- - New: STM32CubeMX is now started more silently (without a splash screen)
- - New: add integration and CLI tests (sort of)
- - New: testing with different Python versions using pyenv (3.6+ target)
- - New: 'run_editor' test is now preliminary automatically check whether an editor is installed on the machine
- - New: more typing annotations
- - Fixed: the app has been failed to start as 'python app.py' (modify sys.path to fix)
- - Changed: 'main' function is now fully modular: can be run from anywhere with given CLI arguments (will be piped forward to be parsed via 'argparse')
- - Changed: rename stm32pio.py -> app.py (stm32pio is the name of the package as a whole)
- - Changed: rename util.py -> lib.py (means main library)
- - Changed: logging is now more modular: we do not set global 'basicConfig' and specify separated loggers for each module instead
- - Changed: more clear description of steps to do for each user subcommand by the code
- - Changed: get rid of 'print' calls leaving only logging messages (easy to turn on/off the console output in the outer code)
- - Changed: re-imagined API behavior: where to raise exceptions, where to return values and so on
- - Changed: more clean API, e.g. move out the board resolving procedure from the 'pio_init' method and so on
- - Changed: test fixture is now moved out from the repo and is deployed temporarily on every test run
- - Changed: set-up and tear-down stages are now done using 'unittest' API
- - Changed: actualized .ioc file for the latest STM32CubeMX version (5.4.0 at the moment)
- - Changed: improved help, docs, comments
-
- ver. 0.95 (15.12.19):
- - New: re-made patch() method: it can intelligently parses platformio.ini and substitute necessary options. Patch can now be a general .INI-format config
- - New: test_get_state()
- - New: upload to PyPI
- - New: use regular expressions to test logging output format for both verbose and normal modes
- - Fix: return -d as an optional argument to be able to execute a short form of the app
- - Changed: subclass ConfigParser to add save() method (remove Stm32pio.save_config())
- - Changed: resolve more TO-DOs (some cannot be achieved actually)
- - Changed: improve setup.py
- - Changed: replace traceback.print to 'logging' functionality
- - Changed: no more mutable default arguments
- - Changed: use inspect.cleandoc to place long multi-line strings in code
- - Changed: rename _load_config_file(), ProjectState.PATCHED
- - Changed: use interpolation=None for ConfigParser
- - Changed: check whether there is already a platformio.ini file and warn in this case on PlatformIO init stage
- - Changed: sort imports in the alphabetic order
- - Changed: use configparser to test project patching
-
- ver. 0.96 (17.12.19):
- - Fix: generate_code() doesn't destroy the temp folder after execution
- - Fix: improved and actualized docs, comments, annotations
- - Changed: print Python interpreter information on testing
- - Changed: move some asserts inside subTest context managers
- - Changed: rename pio_build() => build()
- - Changed: take out to the settings.py the width of field in a log format string
- - Changed: use file statistic to check its size instead of reading the whole content
- - Changed: more logging output
- - Changed: change some methods signatures to return result value
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..cbd8773
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,175 @@
+# stm32pio changelog
+
+## ver. 0.1 (30.11.17)
+ - Initial version
+
+## ver. 0.2 (14.01.18)
+ - New: this changelog and more comments :)
+ - Fixed: compatible with new filename politics (see PlatformIO issue #1107)
+ (`inc` now must be `include` so we add option to `platformio.ini`)
+ - Changed: use os.path.normpath() instead of manually removing trailing `/`
+
+## ver. 0.21 (18.01.18)
+ - New: checking board name before PlatformIO start
+
+## ver. 0.4 (03-04.04.18)
+ - New: hide CubeMX and PlatformIO stdout output
+ - New: shebang
+ - New: choose your favourite editor with `--start-editor` option (replaces `--with-atom`)
+ - New: logging module
+ - New: more checks
+ - New: `settings.py` file
+ - New: cross-platform running
+ - New: debug output (verbose `-v` mode)
+ - New: `README.md` and more comments
+ - Fixed: remove unnecessary imports
+ - Fixed: command to initialize PlatformIO project (remove double quotation marks)
+ - Changed: many architectural improvements
+ - Changed: documentation improvements
+
+## ver. 0.45 (04-05.04.18)
+ - New: introducing unit-tests for the app
+ - New: clean-up feature
+
+## ver. 0.5 (07.04.18)
+ - New: more comments
+ - New: screenshots for the usage example
+ - Fixed: many small fixes and improvements
+ - Changed: test now is more isolated and uses `./stm32pio-test/stm32pio-test.ioc` file
+
+## ver. 0.7 (05-07.11.18)
+ - New: Windows support!
+ - New: new editors support (Sublime Text)
+ - New: more comments and docstrings
+ - New: more checks to improve robustness
+ - New: if `__name__ == '__main__'` block
+ - New: new test: build generated project
+ - New: new test: run editors
+ - New: new test: user's code preservation after the code regeneration
+ - New: clean run for test cases (implemented using decorator)
+ - Fixed: compatible with latest PlatformIO project structure (ver 3.6.1)
+ - Fixed: many small fixes and improvements
+ - Changed: `java_cmd` parameter in `settings.py` (simple `java` by default)
+ - Changed: move to double-quoted strings
+ - Changed: remove `_getProjectNameByPath()` function (replaced by `os.path.basename()`)
+ - Changed: vast f-strings usage
+ - Changed: test `.ioc` file is updated to the latest STM32CubeMX version (4.27.0 at the moment)
+ - Changed: use `os.path.join()` instead of manually composing of paths
+ - Changed: use `with ... as ...` construction for opening files
+ - Changed: 120 chars line width
+ - Changed: PEP 8 conformity: variables and functions naming conventions
+ - Changed: PEP 8 conformity: multi-line imports
+ - Changed: `miscs.py` module is renamed to `util.py`
+
+## ver. 0.73 (10-11.02.19)
+ - New: use more convenient Python project structure
+ - New: package can be install using setuptools
+ - New: TODO list
+ - New: `--directory` option is now optional if the program gets called from the project directory
+ - Fixed: license copyright
+ - Fixed: 'dot' path will be handle successfully now
+ - Fixed: bug on case insensitive machines
+ - Fixed: bug in tests that allowing to pass the test even in failure situation
+ - Changed: test `.ioc` file is updated to the latest STM32CubeMX version (5.0.1 at the moment)
+ - Changed: documentation improvements
+
+## ver. 0.74 (27.02.19)
+ - New: new internal `_get_project_path()` function (more clean main script)
+ - New: optional `--with-build` option for `new` mode allowing to make an initial build to save a time
+ - Changed: `util.py` functions now raising the exceptions instead of forcing the exit
+ - Changed: test `.ioc` file is updated to the latest STM32CubeMX version (5.1.0 at the moment)
+ - Changed: documentation improvements
+
+## ver. 0.8 (09.19)
+ - New: `setup.py` can now install executable script to run `stm32pio` from any location
+ - New: stm32pio logo/schematic
+ - New: add PyCharm to `.gitignore`
+ - New: add clear TODOs for the next release (some sort of a roadmap)
+ - New: single `__version__` reference
+ - New: extended shebang
+ - New: add some new tests (`test_build_should_raise`, `test_file_not_found`)
+ - Fixed: options `--start-editor` and `--with-build` can now be used both for `new` and `generate` commands
+ - Fixed: import scheme is now as it should be
+ - Changed: migrate from `os.path` to `pathlib` as much as possible for paths management (as a more high-level module)
+ - Changed: `start editor` feature is now starting an arbitrary editor (in the same way as you do it from the terminal)
+ - Changed: take outside `platformio` command (to `settings.py`)
+ - Changed: screenshots were actualized for recent CubeMX versions
+ - Changed: logging output in standard (non-verbose) mode is simpler
+ - Changed: move tests in new location
+ - Changed: revised and improved tests
+ - Changed: actualized `.ioc` file and clean-up the code according to the latest STM32CubeMX version (5.3.0 at the moment)
+ - Changed: revised and improved util module
+
+## ver. 0.9 (11-12.19)
+ - New: tested with Python3 version of PlatformIO
+ - New: `__main__.py` file (to run the app as module (`python -m stm32pio`))
+ - New: 'init' subcommand (initialize the project only, useful for the preliminary tweaking)
+ - New: introducing the OOP pattern: we have now a Stm32pio class representing a single project (project path as a main identifier)
+ - New: projects now have a config file stm32pio.ini where the user can set the variety of parameters
+ - New: `state` property calculating the estimated project state on every request to itself (beta). It is the concept for future releases
+ - New: STM32CubeMX is now started more silently (without a splash screen)
+ - New: add integration and CLI tests (sort of)
+ - New: testing with different Python versions using pyenv (3.6+ target)
+ - New: `test_run_editor` is now preliminary automatically checks whether an editor is installed on the machine
+ - New: more typing annotations
+ - Fixed: the app has been failed to start as `python app.py` (modify `sys.path` to fix)
+ - Changed: `main()` function is now fully modular: can be run from anywhere with given CLI arguments (will be piped forward to be parsed via `argparse`)
+ - Changed: rename `stm32pio.py` -> `app.py` (stm32pio is the name of the package as a whole)
+ - Changed: rename `util.py` -> `lib.py` (means core library)
+ - Changed: logging is now more modular: we do not set global `basicConfig` and specify separated loggers for each module instead
+ - Changed: more clear description of steps to do for each user subcommand by the code
+ - Changed: get rid of `print()` calls leaving only logging messages (easy to turn on/off the console output in the outer code)
+ - Changed: reimagined API behavior: where to raise exceptions, where to return values and so on
+ - Changed: more clean API, e.g. move out the board resolving procedure from the `pio_init()` method and so on
+ - Changed: test fixture is now moved out from the repo and is deployed temporarily on every test run
+ - Changed: set-up and tear-down stages are now done using `unittest` API
+ - Changed: actualized `.ioc` file for the latest STM32CubeMX version (5.4.0 at the moment)
+ - Changed: improved help, docs, comments
+
+## ver. 0.95 (15.12.19)
+ - New: re-made `patch()` method: it can intelligently parse `platformio.ini` and substitute necessary options. Patch can now be a general .INI-format config
+ - New: `test_get_state()`
+ - New: upload to PyPI
+ - New: use regular expressions to test logging output format for both verbose and normal modes
+ - Fix: return `-d` as an optional argument to be able to execute a short form of the app
+ - Changed: subclass `ConfigParser` to add `save()` method (remove `Stm32pio.save_config()`)
+ - Changed: resolve more TO-DOs (some cannot be achieved actually)
+ - Changed: improve `setup.py`
+ - Changed: replace traceback.print to `logging` functionality
+ - Changed: no more mutable default arguments
+ - Changed: use `inspect.cleandoc` to place long multi-line strings in code
+ - Changed: rename `_load_config_file()`, `ProjectState.PATCHED`
+ - Changed: use `interpolation=None` on `ConfigParser`
+ - Changed: check whether there is already a `platformio.ini` file and warn in this case on PlatformIO init stage
+ - Changed: sort imports in the alphabetic order
+ - Changed: use `configparser` to test project patching
+
+## ver. 0.96 (17.12.19)
+ - Fix: `generate_code()` doesn't destroy the temp folder after execution
+ - Fix: improved and actualized docs, comments, annotations
+ - Changed: print Python interpreter information on testing
+ - Changed: move some asserts inside subTest context managers
+ - Changed: rename `pio_build()` => `build()`
+ - Changed: take out to the `settings.py` the width of field in a log format string
+ - Changed: use file statistic to check its size instead of reading the whole content
+ - Changed: more logging output
+ - Changed: change some methods signatures to return result value
+
+## ver. 1.0 (XX.03.20)
+ - New: introduce GUI version of the app (beta)
+ - New: redesigned stage-state machinery - integrates seamlessly into both CLI and GUI worlds. Python `Enum` represents a single stage of the project (e.g. "code generated" or "project built") while the special dictionary unfolds the full information about the project i.e. combination of all stages (True/False). Implemented in 2 classes - `ProjectStage` and `ProjectState`, though the `Stm32pio.state` property is intended to be a user's getter. Both classes have human-readable string representations
+ - New: related to previous - `status` CLI command
+ - New: `util.py` module (yes, now the name matches the functionality it provides)
+ - New: logging machinery - adapting for more painless embedding the lib in another code. `logging.Logger` objects are now individual unique attributes of every `Stm32pio` instance so it is possible to distinguish which project is actually produced a message (not so useful for a current CLI version but for other applications, including GUI, is). `LogPipe` context manager is used to redirect `subprocess` output to the `logging` module. `DispatchingFormatter` allows to specify different `logging`' formatters depending on the origin of the log record. Substituted `LogRecordFactory` handles custom flags to `.log()` functions family
+ - Changed: imporoved README
+ - Changed: `platformio` package is added as a requirement and is used for retrieving the boards names (`util.py` -> `get_platformio_boards()`). Expected to become the replacement for all PlatformIO CLI calls
+ - Changed: Markdown markup for this changelog
+ - Changed: bump up `.ioc` file version
+ - Changed: removed final "exit..." log message
+ - Changed: removed `Config` subclass and move its `save()` method back to the main `Stm32pio` class. This change serves 2 goals: ensures consistency in the possible operations list (i.e. `init`, `generate`, etc.) and makes possible to register the function at the object destruction stage via `weakref.finilize()`
+ - Changed: removed `_resolve_board()` method as it is not needed anymore
+ - Changed: renamed `_load_config_file()` -> `_load_config()` (hide implementation details)
+ - Changed: use `logger.isEnabledFor()` instead of manually comparing logging levels
+ - Changed: slightly tuned exceptions (more specific ones where it make sense)
+- Changed: rename `project_path` -> `path`
+- Changed: actualized tests, more broad usage of the `app.main()` function versus `subprocess.run()`
diff --git a/LICENSE b/LICENSE
index 2f9ad41..5ca1604 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020 Andrey Chufyrev aka ussserrr
+Copyright (c) 2018-2020 Andrey Chufyrev aka ussserrr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/MANIFEST.in b/MANIFEST.in
index 3c52153..c43da9a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,8 +1,11 @@
+include .gitignore
+include CHANGELOG
+include LICENSE
include MANIFEST.in
include README.md
-include LICENSE
-include CHANGELOG
include TODO.md
-include .gitignore
+recursive-include screenshots *
recursive-include stm32pio-test-project *
-include screenshots/*.png
+include stm32pio-gui/main.qml
+include stm32pio-gui/README.md
+recursive-include stm32pio-gui/icons *
diff --git a/README.md b/README.md
index 715a31f..218a2e5 100644
--- a/README.md
+++ b/README.md
@@ -1,70 +1,78 @@
# stm32pio
Small cross-platform Python app that can create and update [PlatformIO](https://platformio.org) projects from [STM32CubeMX](https://www.st.com/en/development-tools/stm32cubemx.html) `.ioc` files.
-It uses STM32CubeMX to generate a HAL-framework based code and alongside creates PlatformIO project with the compatible `stm32cube` framework specified.
+It uses STM32CubeMX to generate a HAL-framework-based code and alongside creates PlatformIO project with compatible parameters to stick them both together.
![Logo](/screenshots/logo.png)
## Features
- - Start the new project in a single directory using only an `.ioc` file
- - Update existing project after changing hardware options from CubeMX
+ - Start the new complete project in a single directory using only an `.ioc` file
+ - Update an existing project after changing hardware options in CubeMX
- Clean-up the project (WARNING: it deletes ALL content of project path except the `.ioc` file!)
+ - Get the status information
- *[optional]* Automatically run your favorite editor in the end
- *[optional]* Automatically make an initial build of the project
+ - *[optional]* GUI version (beta) (see stm32pio-gui sub-folder for the dedicated README)
## Requirements:
- For this app:
- Python 3.6 and above
+ - `platformio`
- For usage:
- macOS, Linux, Windows
- - STM32CubeMX (all recent versions) with desired downloaded frameworks (F0, F1, etc.)
+ - STM32CubeMX with desired downloaded frameworks (F0, F1, etc.)
- Java CLI (JRE) (likely is already installed if the STM32CubeMX is working)
- - PlatformIO CLI (already presented if you have installed PlatformIO via some package manager or need to be installed as the command line extension from IDE)
+ - PlatformIO CLI (already presented if you have installed PlatformIO via some package manager or need to be installed as the "command line extension" from IDE)
-A general recommendation there would be to try to generate and build a code manually (via the CubeMX GUI and PlatformIO CLI or IDE) at least once before using stm32pio to make sure that all tools are working properly without any "glue".
+A general recommendation there would be to test both CubeMX (code generation) and PlatformIO (project creation, building) at least once before using stm32pio to make sure that all tools work properly even without any "glue".
## Installation
-Starting from v0.8 it is possible to install the utility to be able to run stm32pio from anywhere. Use
+You can run the app in a portable way by downloading or cloning the snapshot of the repository and invoking the main script or Python module:
+```shell script
+$ python3 stm32pio/app.py
+$ # or
+$ python3 -m stm32pio
+```
+
+(we assume python3 and pip3 hereinafter). It is possible to run the app like this from anywhere.
+
+However, it's handier to install the utility to be able to run stm32pio from anywhere. Use
```shell script
stm32pio-repo/ $ pip3 install .
```
command to launch the setup process. Now you can simply type 'stm32pio' in the terminal to run the utility in any directory.
-PyPI distribution (starting from v0.95):
+Finally, the PyPI distribution (starting from v0.95) is available:
```shell script
$ pip install stm32pio
```
-To uninstall run
+To uninstall in both cases run
```shell script
-$ pip3 uninstall stm32pio
+$ pip uninstall stm32pio
```
## Usage
Basically, you need to follow such a pattern:
- 1. Create CubeMX project, set-up your hardware configuration
- 2. Run stm32pio that automatically invoke CubeMX to generate the code, create PlatformIO project, patch a 'platformio.ini' file and so on
- 3. Work on the project in your editor, compile/upload/debug etc.
- 4. Edit the configuration in CubeMX when necessary, then run stm32pio to regenerate the code.
+ 1. Create CubeMX project (.ioc file), set-up your hardware configuration, save
+ 2. Run the stm32pio that automatically invokes CubeMX to generate the code, creates PlatformIO project, patches a `platformio.ini` file and so on
+ 3. Work on the project in your editor as usual, compile/upload/debug etc.
+ 4. Edit the configuration in CubeMX when necessary, then run stm32pio to re-generate the code.
Refer to Example section on more detailed steps. If you face off with some error try to enable a verbose output to get more information about a problem:
```shell script
$ stm32pio -v [command] [options]
```
-Note, that the patch operation (which takes the CubeMX code and PlatformIO project to the compliance) erases all the comments (lines starting with `;`) inside the `platformio.ini` file. They are not required anyway, in general, but if you need them please consider to save the information somewhere else.
-
-Starting from v0.95, the patch can has a general-form .INI content so it is possible to modify several sections and apply composite patches. This works totally fine for almost every cases except some big complex patches involving the parameters interpolation feature. It is turned off for both `platformio.ini` and user's patch parsing by default. If there are some problems you've met due to a such behavior please modify the source code to match the parameters interpolation kind for the configs you need to. Seems like `platformio.ini` uses `ExtendedInterpolation` for its needs, by the way.
-
On the first run stm32pio will create a config file `stm32pio.ini`, syntax of which is similar to the `platformio.ini`. You can also create this config without any following operations by initializing the project:
```shell script
$ stm32pio init -d path/to/project
```
-It may be useful to tweak some parameters before proceeding. The structure of the config is separated in two sections: `app` and `project`. Options of the first one is related to the global settings such as commands to invoke different instruments though they can be adjusted on the per-project base while the second section contains of project-related parameters. See the comments in the [`settings.py`](/stm32pio/settings.py) file for parameters description.
+It may be useful to tweak some parameters before proceeding. The structure of the config is separated in two sections: `app` and `project`. Options of the first one is related to the global settings such as commands to invoke different instruments though they can be adjusted on the per-project base while the second section contains of project-related parameters. See comments in the [`settings.py`](/stm32pio/settings.py) file for parameters description.
You can always run
```shell script
@@ -72,11 +80,19 @@ $ python3 app.py --help
```
to see help on available commands.
-You can also use stm32pio as a package and embed it in your own application. See [`app.py`](/stm32pio/app.py) to see how to implement this. Basically you need to import `stm32pio.lib` module (where the main `Stm32pio` class resides), set up a logger and you are good to go. If you need higher-level API similar to the CLI version, use `main()` function in `app.py` passing the same CLI arguments to it (except the actual script name).
+### Project patching
+
+Note, that the patch operation (which takes the CubeMX code and PlatformIO project to the compliance) erases all the comments (lines starting with `;`) inside the `platformio.ini` file. They are not required anyway, in general, but if you need them for some reason please consider to save the information somewhere else.
+
+For those who want to modify the patch (default one is at [`settings.py`](/stm32pio/settings.py), project one in a config file `stm32pio.ini`): it can has a general-form .INI content so it is possible to specify several sections and apply composite patches. This works totally fine for the most cases except, perhaps, some really big complex patches involving, say, the parameters interpolation feature. It is turned off for both `platformio.ini` and user's patch parsing by default. If there are some problems you've met due to a such behavior please modify the source code to match the parameters interpolation kind for the configs you need to. Seems like `platformio.ini` uses `ExtendedInterpolation` for its needs, by the way.
+
+### Embedding
+
+You can also use stm32pio as an ordinary Python package and embed it in your own application. Take a look at the CLI ([`app.py`](/stm32pio/app.py)) or GUI versions to see some possible ways of implementing this. Basically, you need to import `stm32pio.lib` module (where the main `Stm32pio` class resides), (optionally) set up a logger and you are good to go. If you prefer higher-level API similar to the CLI version, use `main()` function in `app.py` passing the same CLI arguments to it (except the actual script name).
## Example
-1. Run CubeMX, choose MCU/board, do all necessary stuff
+1. Run CubeMX, choose MCU/board, do all necessary tweaking
2. Select `Project Manager -> Project` tab, specify "Project Name", choose "Other Toolchains (GPDSC)". In `Code Generator` tab check "Copy only the necessary library files" and "Generate periphery initialization as a pair of '.c/.h' files per peripheral" options
![Code Generator tab](/screenshots/tab_CodeGenerator.png)
@@ -89,21 +105,22 @@ You can also use stm32pio as a package and embed it in your own application. See
5. Run `platformio boards` (`pio boards`) or go to [boards](https://docs.platformio.org/en/latest/boards) to list all supported devices. Pick one and use its ID as a `-b` argument (for example, `nucleo_f031k6`)
6. All done! You can now run
```shell script
- $ python3 app.py new -d path/to/cubemx/project/ -b nucleo_f031k6 --start-editor=code --with-build
+ $ stm32pio new -d path/to/cubemx/project/ -b nucleo_f031k6 --start-editor=code --with-build
```
- to complete generation, start the Visual Studio Code editor with opened folder and compile the project (as an example, not required). Make sure you have all tools in PATH (`java` (or set its path in `stm32pio.ini`), `platformio`, `python`, editor). You can use shorter form if you are already located in the project directory (also using shebang alias):
+ to trigger the code generation, compile the project and start the VSCode editor with opened folder (last 2 options are given as an example and they are not required). Make sure you have all the tools in PATH (`java` (or set its path in `stm32pio.ini`), `platformio`, `python`, editor). You can use a slightly shorter form if you are already located in the project directory:
```shell script
path/to/cubemx/project/ $ stm32pio new -b nucleo_f031k6
```
-7. If you will be in need to update hardware configuration in the future, make all necessary stuff in CubeMX and run `generate` command in a similar way:
+7. To get the information about the current state of the project use `status` command.
+8. If you will be in need to update hardware configuration in the future, make all the necessary stuff in CubeMX and run `generate` command in a similar way:
```shell script
- $ python3 app.py generate -d /path/to/cubemx/project
+ $ stm32pio generate -d /path/to/cubemx/project
```
-8. To clean-up the folder and keep only the `.ioc` file run `clean` command
+9. To clean-up the folder and keep only the `.ioc` file run `clean` command.
## Testing
-Since ver. 0.45 there are some tests in file [`test.py`](/stm32pio/tests/test.py) (based on the unittest module). Run
+There are some tests in file [`test.py`](/stm32pio/tests/test.py) (based on the unittest module). Run
```shell script
stm32pio-repo/ $ python3 -m unittest -b -v
```
@@ -111,18 +128,14 @@ or
```shell script
stm32pio-repo/ $ python3 -m stm32pio.tests.test -b -v
```
-to test the app. It uses STM32F0 framework to generate and build a code from the test [`stm32pio-test-project.ioc`](/stm32pio-test-project/stm32pio-test-project.ioc) project file. Please make sure that the test project folder is clean (i.e. contains only an .ioc file) before running the test.
+to test the app. It uses STM32F0 framework to generate and build a code from the test [`stm32pio-test-project.ioc`](/stm32pio-test-project/stm32pio-test-project.ioc) project file. Please make sure that the test project folder is clean (i.e. contains only an .ioc file) before running the test otherwise it can lead to some cases failing.
-For specific test suite or case you can use
+For the specific test suite or case you can use
```shell script
stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestIntegration -b -v
stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestCLI.test_verbose -b -v
```
-While testing was performed on different Python and OS versions, some older Windows versions had shown some 'glitches' and instability. [WinError 5] and others had appeared on such tests like `test_run_edtor` and on `tempfile` clean-up processes. So be ready to face off with them.
-
-CI is hard to implement for all target OSes during the requirement to have all the tools (PlatformIO, Java, CubeMX, etc.) installed during the test. For example, ST doesn't even provide a direct link to the CubeMX for downloading.
-
## Restrictions
- The tool doesn't check for different parameters compatibility, e.g. CPU frequency, memory sizes and so on. It simply eases your workflow with these 2 programs (PlatformIO and STM32CubeMX) a little bit.
@@ -131,3 +144,4 @@ CI is hard to implement for all target OSes during the requirement to have all t
lib_extra_dirs = Middlewares/Third_Party/FreeRTOS
```
You also need to move all `.c`/`.h` files to the appropriate folders respectively. See PlatformIO documentation for more information.
+ - The project folder, once instantiated, is not portable i.e. if you move it at some other place and invoke stm32pio it will report you an error. This because `stm32pio.ini` config is currently stores absolute paths instead of relative.
diff --git a/TODO.md b/TODO.md
index fd0185a..a16fa55 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,36 +3,34 @@
- [ ] Middleware support (FreeRTOS, etc.)
- [ ] Arduino framework support (needs research to check if it is possible)
- [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on...
- - [x] Function annotations
- - [ ] GUI. For example, drop the folder into small window (with checkboxes corresponding with CLI options) and get the output. At left is a list of recent projects
- - [ ] GUI. Indicate the progress as states goes forward during the run (see `scratch.py`)
- - [x] Remade as Class (constructor `__init__(project_path)`)
- - [x] Config file for every project instead of the `settings.py` (but we still sill be storing the default parameters there)
- - [x] Test CLI (integration testing)
- - [x] Move test fixtures out of the 'tests' so we can use it for multiple projects (for example while testing CLI and GUI versions). Set up test folder for every single test so we make sure the .ioc file is always present and not deleted after failed test
- - [ ] Upload to PyPI
- - [x] `__main__`
- - [x] Abort `--with-build` if no platformio.ini file is present
- - [x] Rename `stm32pio.py` -> `app.py`
- - [x] Rename `util.py` -> `lib.py` (maybe)
- - [x] Do not require matching of the project folder and .ioc file names (use first .ioc file found)
- - [x] Remove casts to string where we can use path-like objects
- - [x] Settings string templates and multi line
- - [x] Smart `start_editor` test (detect editors in system, maybe use unittest `skipIf` decorator)
- - [x] `init` command
- - [x] New argparse algo cause now we have config file
- - [x] Update `.ioc` file
- - [x] `str(path)` -> `path` were possible
- - [x] Check `start_editor()` for different input
- - [x] Test on Python 3.6 (pyenv)
- - [x] Test for `get_state()` (as sequence of states (see scratch.py))
- - [x] Remake `get_state()` as property value (read-only getter with decorator)
- - [x] Try to invoke stm32pio as module (-m), from different paths...
- - [x] Logs format test (see prepared regular expressions)
+ - [ ] GUI. Tests (research approaches and patterns)
+ - [ ] GUI. Reduce number of calls to 'state' (many IO operations)
+ - [ ] GUI. Drag and drop the new folder into the app window
+ - [ ] VSCode plugin
+ - [x] Remove casts to string where we can use path-like objects (related to Python version as new ones receive path-like objects arguments)
- [ ] We look for some snippets of strings in logs and output for the testing code but we hard-code them and this is not good, probably
- - [x] Do we really need *sys.exc_info() ?
- - [x] See logging.exception and sys_exc argument for logging.debug
- - [x] Make `save_config()` a part of the `config` i.e. `project.config.save()` (subclass `ConfigParser`)
- [ ] Store an initial folder content in .ini config and ignore it on clean-up process. Allow the user to modify such list. Ask the confirmation of a user by-defualt and add additional option for quiet performance
- - [ ] 'status' CLI subcommand, why not?..
+ - [ ] check for all tools (CubeMX, ...) to be present in the system (both CLI and GUI)
- [ ] exclude tests from the bundle (see `setup.py` options)
+ - [ ] generate code docs (help user to understand an internal mechanics, e.g. for embedding). Can be uploaded to the GitHub Wiki
+ - [ ] colored logs, maybe...
+ - [ ] if we require `platformio` package as a dependency we probably can rely on its dependencies too
+ - [ ] check logging work when embed stm32pio lib in third-party stuff (no logging setup at all)
+ - [ ] merge subprocess pipes to one where suitable (i.e. `stdout` and `stderr`)
+ - [ ] redirect subprocess pipes to `DEVNULL` where suitable to suppress output
+ - [ ] some `stm32pio.ini` config file validation
+ - [ ] CHANGELOG markdown markup
+ - [ ] Two words about a synchronous nature of the lib and user's responsibility of async wrapping (if needed). Also, maybe migrate to async/await approach in the future
+ - [ ] `shlex` for `build` command option sanitizing
+ - [ ] `__init__`' `parameters` dict argument schema (Python 3.8 feature). Also, maybe move `save_on_desctruction` parameter there. Maybe separate on `project_params` and `instance_opts`
+ - [ ] General algo of merging a given dict of parameters with the saved one on project initialization
+ - [ ] parse `platformio.ini` to check its correctness in state getter
+ - [ ] CubeMX 0 return code doesn't necessarily means the correct generation (e.g. migration dialog has appeared and 'Cancel' was chosen, or CubeMX_version < ioc_file_version), probably should somehow analyze the output (logs can be parsed. i.e. 2020-03-05 12:08:40,765 \[ERROR\] MainProjectManager:806 - Program Manager : The version of the current IOC is too high.)
+ - [ ] Dispatch tests on several files (too many code actually)
+ - [ ] Do not store absolute paths in config file and make a project portable (use configparser parameters interpolation). Handle renaming
+ - [ ] See https://docs.python.org/3/howto/logging-cookbook.html#context-info to maybe replace current scheme
+ - [ ] UML diagrams (core, GUI back- and front-ends)
+ - [ ] CI is possible
+ - [ ] Test preserving user files and folders on regeneration and mb other operations
+ - [ ] Move special formatters inside the library. It is an implementation detail actually that we use subprocesses and so on
+ - [ ] Mb clean the test project tree before running the tests
diff --git a/setup.py b/setup.py
index 1ed8350..53381e8 100644
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,17 @@
+"""
+To pack:
+ $ pip3 install wheel
+ $ python3 setup.py sdist bdist_wheel
+
+To upload to PyPI:
+ $ python3 -m twine upload dist/*
+"""
+
import setuptools
import stm32pio.app
-with open("README.md", 'r') as readme:
+with open('README.md', 'r') as readme:
long_description = readme.read()
setuptools.setup(
@@ -11,10 +20,10 @@
author='ussserrr',
author_email='andrei4.2008@gmail.com',
description="Small cross-platform Python app that can create and update PlatformIO projects from STM32CubeMX .ioc "
- "files. It uses STM32CubeMX to generate a HAL-framework based code and alongside creates PlatformIO "
- "project with the compatible framework specified",
+ "files. It uses STM32CubeMX to generate a HAL-framework-based code and alongside creates PlatformIO "
+ "project with compatible parameters to stick them both together",
long_description=long_description,
- long_description_content_type="text/markdown",
+ long_description_content_type='text/markdown',
url="https://github.com/ussserrr/stm32pio",
packages=setuptools.find_packages(),
classifiers=[
@@ -25,7 +34,19 @@
"Environment :: Console",
"Topic :: Software Development :: Embedded Systems"
],
+ keywords=[
+ 'platformio',
+ 'stm32',
+ 'stm32cubemx',
+ 'cubemx'
+ ],
python_requires='>=3.6',
+ setup_requires=[
+ 'wheel'
+ ],
+ install_requires=[
+ 'platformio'
+ ],
include_package_data=True,
entry_points={
'console_scripts': [
diff --git a/stm32pio-gui/README.md b/stm32pio-gui/README.md
new file mode 100644
index 0000000..d8a1ac4
--- /dev/null
+++ b/stm32pio-gui/README.md
@@ -0,0 +1,15 @@
+# stm32pio-gui
+
+The cross-platform GUI version of the application. It wraps the core library functionality in the Qt-QML skin using PySide2 (aka "Qt for Python" project) adding projects management feature so you can store and manipulate multiple stm32pio projects in one place.
+
+Currently, it is in a beta stage though all implemented features work, with more or less (mostly visual and architectural) flaws.
+
+
+## Installation
+
+The app requires PySide2 5.12+ package (Qt 5.12 respectively). It is available in all major package managers including pip, apt, brew and so on. More convenient installation process is coming in next releases.
+
+
+## Usage
+
+Enter `python3 app.py` to start the app. Projects list (not the projects themself) and settings are stored by QSettings so refer to its docs if you bother about the actual location.
diff --git a/stm32pio-gui/__init__.py b/stm32pio-gui/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py
new file mode 100644
index 0000000..57bfc43
--- /dev/null
+++ b/stm32pio-gui/app.py
@@ -0,0 +1,501 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# from __future__ import annotations
+
+import collections
+import logging
+import pathlib
+import sys
+import threading
+import time
+import weakref
+
+sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent))
+import stm32pio.settings
+import stm32pio.lib
+import stm32pio.util
+
+from PySide2.QtCore import QUrl, Property, QAbstractListModel, QModelIndex, QObject, Qt, Slot, Signal, QThread,\
+ qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg, QThreadPool, QRunnable,\
+ QStringListModel, QSettings
+if stm32pio.settings.my_os == 'Linux':
+ # Most UNIX systems does not provide QtDialogs implementation...
+ from PySide2.QtWidgets import QApplication
+else:
+ from PySide2.QtGui import QGuiApplication
+from PySide2.QtGui import QIcon
+from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine
+
+
+
+
+
+class BufferedLoggingHandler(logging.Handler):
+ """
+ Simple logging.Handler subclass putting all incoming records into the given buffer
+ """
+ def __init__(self, buffer: collections.deque):
+ super().__init__()
+ self.buffer = buffer
+
+ def emit(self, record: logging.LogRecord) -> None:
+ self.buffer.append(record)
+
+
+class LoggingWorker(QObject):
+ """
+ QObject living in a separate QThread, logging everything it receiving. Intended to be an attached Stm32pio project
+ class property. Stringifies log records using DispatchingFormatter and passes them via Signal interface so they can
+ be conveniently received by any Qt entity. Also, the level of the message is attaching so the reader can interpret
+ them differently.
+
+ Can be controlled by two threading.Event's:
+ stopped - on activation, leads to thread termination
+ can_flush_log - use this to temporarily save the logs in an internal buffer while waiting for some event to occurs
+ (for example GUI widgets to load), and then flush them when time has come
+ """
+
+ sendLog = Signal(str, int)
+
+ def __init__(self, logger: logging.Logger, parent: QObject = None):
+ super().__init__(parent=parent)
+
+ self.buffer = collections.deque()
+ self.stopped = threading.Event()
+ self.can_flush_log = threading.Event()
+ self.logging_handler = BufferedLoggingHandler(self.buffer)
+
+ logger.addHandler(self.logging_handler)
+ self.logging_handler.setFormatter(stm32pio.util.DispatchingFormatter(
+ f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s",
+ special=stm32pio.util.special_formatters))
+
+ self.thread = QThread()
+ self.moveToThread(self.thread)
+
+ self.thread.started.connect(self.routine)
+ self.thread.start()
+
+ def routine(self) -> None:
+ """
+ The worker constantly querying the buffer on the new log messages availability.
+ """
+ while not self.stopped.wait(timeout=0.050):
+ if self.can_flush_log.is_set():
+ if len(self.buffer):
+ record = self.buffer.popleft()
+ self.sendLog.emit(self.logging_handler.format(record), record.levelno)
+ module_logger.debug('exit logging worker')
+ self.thread.quit()
+
+
+
+class ProjectListItem(QObject):
+ """
+ The core functionality class - GUI representation of the Stm32pio project
+ """
+
+ nameChanged = Signal() # properties notifiers
+ stateChanged = Signal()
+ stageChanged = Signal()
+
+ logAdded = Signal(str, int, arguments=['message', 'level']) # send the log message to the front-end
+ actionDone = Signal(str, bool, arguments=['action', 'success']) # emit when the action has executed
+
+
+ def __init__(self, project_args: list = None, project_kwargs: dict = None, parent: QObject = None):
+ super().__init__(parent=parent)
+
+ self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}")
+ self.logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO)
+ self.logging_worker = LoggingWorker(self.logger)
+ self.logging_worker.sendLog.connect(self.logAdded)
+
+ # QThreadPool can automatically queue new incoming tasks if a number of them are larger than maxThreadCount
+ self.workers_pool = QThreadPool()
+ self.workers_pool.setMaxThreadCount(1)
+ self.workers_pool.setExpiryTimeout(-1) # tasks forever wait for the available spot
+
+ # These values are valid till the Stm32pio project does not initialize itself (or failed to)
+ self.project = None
+ self._name = 'Loading...'
+ self._state = { 'LOADING': True }
+ self._current_stage = 'Loading...'
+
+ self.qml_ready = threading.Event() # the front and the back both should know when each other is initialized
+
+ self._finalizer = weakref.finalize(self, self.at_exit) # register some kind of deconstruction handler
+
+ if project_args is not None:
+ if 'logger' not in project_kwargs:
+ project_kwargs['logger'] = self.logger
+
+ # Start the Stm32pio part initialization right after. It can take some time so we schedule it in a dedicated
+ # thread
+ self.init_thread = threading.Thread(target=self.init_project, args=project_args, kwargs=project_kwargs)
+ self.init_thread.start()
+
+
+ def init_project(self, *args, **kwargs) -> None:
+ """
+ Initialize the underlying Stm32pio project.
+
+ Args:
+ *args: positional arguments of the Stm32pio constructor
+ **kwargs: keyword arguments of the Stm32pio constructor
+ """
+ try:
+ self.project = stm32pio.lib.Stm32pio(*args, **kwargs) # our slightly tweaked subclass
+ except Exception as e:
+ # Error during the initialization
+ self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG))
+ if len(args):
+ self._name = args[0] # use project path string (probably) as a name
+ else:
+ self._name = 'No name'
+ self._state = { 'INIT_ERROR': True }
+ self._current_stage = 'Initializing error'
+ else:
+ self._name = 'Project' # successful initialization. These values should not be used anymore
+ self._state = {}
+ self._current_stage = 'Initialized'
+ finally:
+ self.qml_ready.wait() # wait for the GUI to initialized
+ self.nameChanged.emit() # in any case we should notify the GUI part about the initialization ending
+ self.stageChanged.emit()
+ self.stateChanged.emit()
+
+ def at_exit(self):
+ module_logger.info(f"destroy {self.project}")
+ self.workers_pool.waitForDone(msecs=-1) # wait for all jobs to complete. Currently, we cannot abort them gracefully
+ self.logging_worker.stopped.set() # post the event in the logging worker to inform it...
+ self.logging_worker.thread.wait() # ...and wait for it to exit
+
+ @Property(str, notify=nameChanged)
+ def name(self):
+ if self.project is not None:
+ return self.project.path.name
+ else:
+ return self._name
+
+ @Property('QVariant', notify=stateChanged)
+ def state(self):
+ if self.project is not None:
+ # Convert to normal dict (JavaScript object) and exclude UNDEFINED key
+ return { stage.name: value for stage, value in self.project.state.items()
+ if stage != stm32pio.lib.ProjectStage.UNDEFINED }
+ else:
+ return self._state
+
+ @Property(str, notify=stageChanged)
+ def current_stage(self):
+ if self.project is not None:
+ return str(self.project.state.current_stage)
+ else:
+ return self._current_stage
+
+
+ @Slot()
+ def qmlLoaded(self):
+ """
+ Event signaling the complete loading of needed frontend components.
+ """
+ self.qml_ready.set()
+ self.logging_worker.can_flush_log.set()
+
+
+ @Slot(str, 'QVariantList')
+ def run(self, action: str, args: list):
+ """
+ Asynchronously perform Stm32pio actions (generate, build, etc.) (dispatch all business logic).
+
+ Args:
+ action: method name of the corresponding Stm32pio action
+ args: list of positional arguments for the action
+ """
+
+ worker = ProjectActionWorker(getattr(self.project, action), args, self.logger)
+ worker.actionDone.connect(self.stateChanged)
+ worker.actionDone.connect(self.stageChanged)
+ worker.actionDone.connect(self.actionDone)
+
+ self.workers_pool.start(worker) # will automatically place to the queue
+
+
+
+class ProjectActionWorker(QObject, QRunnable):
+ """
+ Generic worker for asynchronous processes. QObject + QRunnable combination. First allows to attach Qt signals,
+ second is compatible with QThreadPool.
+ """
+
+ actionDone = Signal(str, bool, arguments=['action', 'success'])
+
+ def __init__(self, func, args: list = None, logger: logging.Logger = None, parent: QObject = None):
+ QObject.__init__(self, parent=parent)
+ QRunnable.__init__(self)
+
+ self.logger = logger
+ self.func = func
+ if args is None:
+ self.args = []
+ else:
+ self.args = args
+ self.name = func.__name__
+
+ def run(self):
+ try:
+ result = self.func(*self.args)
+ except Exception as e:
+ if self.logger is not None:
+ self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG))
+ result = -1
+ if result is None or (type(result) == int and result == 0):
+ success = True
+ else:
+ success = False
+ self.actionDone.emit(self.name, success) # notify the caller
+
+
+
+
+class ProjectsList(QAbstractListModel):
+ """
+ QAbstractListModel implementation - describe basic operations and delegate all main functionality to the
+ ProjectListItem.
+ """
+
+ def __init__(self, projects: list = None, parent: QObject = None):
+ """
+ Args:
+ projects: initial list of projects
+ parent: QObject to be parented to
+ """
+ super().__init__(parent=parent)
+ self.projects = projects if projects is not None else []
+
+ @Slot(int, result=ProjectListItem)
+ def getProject(self, index: int):
+ """
+ Expose the ProjectListItem to the GUI QML side. You should firstly register the returning type using
+ qmlRegisterType or similar.
+ """
+ if index in range(len(self.projects)):
+ return self.projects[index]
+
+ def rowCount(self, parent=None, *args, **kwargs):
+ return len(self.projects)
+
+ def data(self, index: QModelIndex, role=None):
+ if role == Qt.DisplayRole or role is None:
+ return self.projects[index.row()]
+
+ def addProject(self, project: ProjectListItem):
+ """
+ Append already formed ProjectListItem to the projects list
+ """
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
+ self.projects.append(project)
+ self.endInsertRows()
+
+ @Slot(QUrl)
+ def addProjectByPath(self, path: QUrl):
+ """
+ Create, append and save in QSettings a new ProjectListItem instance with a given QUrl path (typically sent from
+ the QML GUI).
+
+ Args:
+ path: QUrl path to the project folder (absolute by default)
+ """
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
+ project = ProjectListItem(project_args=[path.toLocalFile()], project_kwargs=dict(save_on_destruction=False), parent=self)
+ self.projects.append(project)
+
+ settings.beginGroup('app')
+ settings.beginWriteArray('projects')
+ settings.setArrayIndex(len(self.projects) - 1)
+ settings.setValue('path', path.toLocalFile())
+ settings.endArray()
+ settings.endGroup()
+
+ self.endInsertRows()
+
+ @Slot(int)
+ def removeProject(self, index: int):
+ """
+ Remove the project residing on the index both from the runtime list and QSettings
+ """
+ if index in range(len(self.projects)):
+ self.beginRemoveRows(QModelIndex(), index, index)
+
+ project = self.projects.pop(index)
+ # TODO: destruct both Qt and Python objects (seems like now they are not destroyed till the program termination)
+
+ settings.beginGroup('app')
+
+ # Get current settings ...
+ settings_projects_list = []
+ for idx in range(settings.beginReadArray('projects')):
+ settings.setArrayIndex(idx)
+ settings_projects_list.append(settings.value('path'))
+ settings.endArray()
+
+ # ... drop the index ...
+ settings_projects_list.pop(index)
+ settings.remove('projects')
+
+ # ... and overwrite the list. We don't use self.projects[i].project.path as there is a chance that 'path'
+ # doesn't exist (e.g. not initialized for some reason project)
+ settings.beginWriteArray('projects')
+ for idx in range(len(settings_projects_list)):
+ settings.setArrayIndex(idx)
+ settings.setValue('path', settings_projects_list[idx])
+ settings.endArray()
+
+ settings.endGroup()
+
+ self.endRemoveRows()
+
+
+
+
+def qt_message_handler(mode, context, message):
+ if mode == QtInfoMsg:
+ mode = logging.INFO
+ elif mode == QtWarningMsg:
+ mode = logging.WARNING
+ elif mode == QtCriticalMsg:
+ mode = logging.ERROR
+ elif mode == QtFatalMsg:
+ mode = logging.CRITICAL
+ else:
+ mode = logging.DEBUG
+ qml_logger.log(mode, message)
+
+
+
+class Settings(QSettings):
+ """
+ Extend the class by useful get/set methods allowing to avoid redundant code lines and also are callable from the
+ QML side. Also, retrieve settings on creation.
+ """
+
+ DEFAULT_SETTINGS = {
+ 'editor': '',
+ 'verbose': False
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ for key, value in self.DEFAULT_SETTINGS.items():
+ if not self.contains('app/settings/' + key):
+ self.setValue('app/settings/' + key, value)
+
+ @Slot(str, result='QVariant')
+ def get(self, key):
+ return self.value('app/settings/' + key)
+
+ @Slot(str, 'QVariant')
+ def set(self, key, value):
+ self.setValue('app/settings/' + key, value)
+
+ if key == 'verbose':
+ module_logger.setLevel(logging.DEBUG if value else logging.INFO)
+ for project in projects_model.projects:
+ project.logger.setLevel(logging.DEBUG if value else logging.INFO)
+
+
+if __name__ == '__main__':
+ # Use it as a console logger for whatever you want to
+ module_logger = logging.getLogger(__name__)
+ module_log_handler = logging.StreamHandler()
+ module_log_handler.setFormatter(logging.Formatter("%(levelname)s %(funcName)s %(message)s"))
+ module_logger.addHandler(module_log_handler)
+ module_logger.setLevel(logging.INFO)
+ module_logger.info('Starting stm32pio-gui...')
+
+ # Apparently Windows version of PySide2 doesn't have QML logging feature turn on so we fill this gap
+ # TODO: set up for other platforms too (separate console.debug, console.warn, etc.)
+ if stm32pio.settings.my_os == 'Windows':
+ qml_logger = logging.getLogger('qml')
+ qml_log_handler = logging.StreamHandler()
+ qml_log_handler.setFormatter(logging.Formatter("[QML] %(levelname)s %(message)s"))
+ qml_logger.addHandler(qml_log_handler)
+ qInstallMessageHandler(qt_message_handler)
+
+ # Most Linux distros should be linked with the QWidgets' QApplication instead of the QGuiApplication to enable
+ # QtDialogs
+ if stm32pio.settings.my_os == 'Linux':
+ app = QApplication(sys.argv)
+ else:
+ app = QGuiApplication(sys.argv)
+
+ # Used as a settings identifier too
+ app.setOrganizationName('ussserrr')
+ app.setApplicationName('stm32pio')
+ app.setWindowIcon(QIcon('stm32pio-gui/icons/icon.svg'))
+
+ settings = Settings(parent=app)
+ # settings.remove('app/settings')
+ # settings.remove('app/projects')
+
+ module_logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO)
+
+ settings.beginGroup('app')
+ projects_paths = []
+ for index in range(settings.beginReadArray('projects')):
+ settings.setArrayIndex(index)
+ projects_paths.append(settings.value('path'))
+ settings.endArray()
+ settings.endGroup()
+
+ engine = QQmlApplicationEngine(parent=app)
+
+ qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem')
+ qmlRegisterType(Settings, 'Settings', 1, 0, 'Settings')
+
+ projects_model = ProjectsList(parent=engine)
+ boards = []
+ boards_model = QStringListModel(parent=engine)
+
+ engine.rootContext().setContextProperty('Logging', {
+ logging.getLevelName(logging.CRITICAL): logging.CRITICAL,
+ logging.getLevelName(logging.ERROR): logging.ERROR,
+ logging.getLevelName(logging.WARNING): logging.WARNING,
+ logging.getLevelName(logging.INFO): logging.INFO,
+ logging.getLevelName(logging.DEBUG): logging.DEBUG,
+ logging.getLevelName(logging.NOTSET): logging.NOTSET
+ })
+ engine.rootContext().setContextProperty('projectsModel', projects_model)
+ engine.rootContext().setContextProperty('boardsModel', boards_model)
+ engine.rootContext().setContextProperty('appSettings', settings)
+
+ engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml'))
+
+ main_window = engine.rootObjects()[0]
+
+
+ # Getting PlatformIO boards can take long time when the PlatformIO cache is outdated but it is important to have
+ # them before the projects list restoring, so we start a dedicated loading thread. We actually can add other
+ # start-up operations here if there will be need to. Use the same ProjectActionWorker to spawn the thread at pool.
+
+ def loading():
+ global boards
+ boards = ['None'] + stm32pio.util.get_platformio_boards()
+
+ def on_loading(_, success):
+ # TODO: somehow handle an initialization error
+ boards_model.setStringList(boards)
+ projects = [ProjectListItem(project_args=[path], project_kwargs=dict(save_on_destruction=False), parent=projects_model)
+ for path in projects_paths]
+ for p in projects:
+ projects_model.addProject(p)
+ main_window.backendLoaded.emit() # inform the GUI
+
+ loader = ProjectActionWorker(loading, logger=module_logger)
+ loader.actionDone.connect(on_loading)
+ QThreadPool.globalInstance().start(loader)
+
+
+ sys.exit(app.exec_())
diff --git a/stm32pio-gui/icons/LICENSE b/stm32pio-gui/icons/LICENSE
new file mode 100644
index 0000000..9aa1c60
--- /dev/null
+++ b/stm32pio-gui/icons/LICENSE
@@ -0,0 +1 @@
+Icons by Flat Icons, Google from www.flaticon.com
diff --git a/stm32pio-gui/icons/add.svg b/stm32pio-gui/icons/add.svg
new file mode 100644
index 0000000..7a55656
--- /dev/null
+++ b/stm32pio-gui/icons/add.svg
@@ -0,0 +1,41 @@
+
+
+
diff --git a/stm32pio-gui/icons/icon.svg b/stm32pio-gui/icons/icon.svg
new file mode 100644
index 0000000..3f264b0
--- /dev/null
+++ b/stm32pio-gui/icons/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/stm32pio-gui/icons/remove.svg b/stm32pio-gui/icons/remove.svg
new file mode 100644
index 0000000..0445e5c
--- /dev/null
+++ b/stm32pio-gui/icons/remove.svg
@@ -0,0 +1,40 @@
+
+
+
diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml
new file mode 100644
index 0000000..3129a79
--- /dev/null
+++ b/stm32pio-gui/main.qml
@@ -0,0 +1,779 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import QtGraphicalEffects 1.12
+import QtQuick.Dialogs 1.3 as QtDialogs
+
+import Qt.labs.platform 1.1 as QtLabs
+
+import ProjectListItem 1.0
+import Settings 1.0
+
+
+ApplicationWindow {
+ id: mainWindow
+ visible: true
+ minimumWidth: 980 // comfortable initial size
+ minimumHeight: 300
+ height: 530
+ title: 'stm32pio'
+ color: 'whitesmoke'
+
+ /*
+ Notify the front-end about the end of an initial loading
+ */
+ signal backendLoaded()
+ onBackendLoaded: loadingOverlay.close()
+ Popup {
+ id: loadingOverlay
+ visible: true
+ parent: Overlay.overlay
+ anchors.centerIn: Overlay.overlay
+ modal: true
+ background: Rectangle { opacity: 0.0 }
+ closePolicy: Popup.NoAutoClose
+
+ contentItem: Column {
+ BusyIndicator {}
+ Text { text: 'Loading...' }
+ }
+ }
+
+ /*
+ Slightly customized QSettings
+ */
+ property Settings settings: appSettings
+ QtDialogs.Dialog {
+ id: settingsDialog
+ title: 'Settings'
+ standardButtons: QtDialogs.StandardButton.Save | QtDialogs.StandardButton.Cancel
+ GridLayout {
+ columns: 2
+
+ Label {
+ text: 'Editor'
+ Layout.preferredWidth: 140
+ }
+ TextField {
+ id: editor
+ text: settings.get('editor')
+ }
+
+ Label {
+ text: 'Verbose output'
+ Layout.preferredWidth: 140
+ }
+ CheckBox {
+ id: verbose
+ leftPadding: -3
+ checked: settings.get('verbose')
+ }
+ }
+ onAccepted: {
+ settings.set('editor', editor.text);
+ settings.set('verbose', verbose.checked);
+ }
+ }
+
+ QtDialogs.Dialog {
+ id: aboutDialog
+ title: 'About'
+ standardButtons: QtDialogs.StandardButton.Close
+ ColumnLayout {
+ Rectangle {
+ width: 250
+ height: 100
+ TextArea {
+ width: parent.width
+ height: parent.height
+ readOnly: true
+ selectByMouse: true
+ wrapMode: Text.WordWrap
+ textFormat: TextEdit.RichText
+ horizontalAlignment: TextEdit.AlignHCenter
+ verticalAlignment: TextEdit.AlignVCenter
+ text: `2018 - 2020 © ussserrr
+ GitHub`
+ onLinkActivated: Qt.openUrlExternally(link)
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.NoButton // we don't want to eat clicks on the Text
+ cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ Project representation is, in fact, split in two main parts: one in a list and one is an actual workspace.
+ To avoid some possible bloopers we should make sure that both of them are loaded before performing
+ any actions with the project. To not reveal QML-side implementation details to the backend we define
+ this helper function that counts number of widgets currently loaded for each project in model and informs
+ the Qt-side right after all necessary components went ready.
+ */
+ property var initInfo: ({})
+ function setInitInfo(projectIndex) {
+ if (projectIndex in initInfo) {
+ initInfo[projectIndex]++;
+ } else {
+ initInfo[projectIndex] = 1;
+ }
+
+ if (initInfo[projectIndex] === 2) {
+ delete initInfo[projectIndex]; // index can be reused
+ projectsModel.getProject(projectIndex).qmlLoaded();
+ }
+ }
+
+ // TODO: fix (jumps skipping next)
+ function moveToNextAndRemove() {
+ // Select and go to some adjacent index before deleting the current project. -1 is a correct
+ // QML index (note that for Python it can jump to the end of the list, ensure a consistency!)
+ const indexToRemove = projectsListView.currentIndex;
+ let indexToMoveTo;
+ if (indexToRemove === (projectsListView.count - 1)) {
+ indexToMoveTo = indexToRemove - 1;
+ } else {
+ indexToMoveTo = indexToRemove + 1;
+ }
+
+ projectsListView.currentIndex = indexToMoveTo;
+ projectsWorkspaceView.currentIndex = indexToMoveTo;
+
+ projectsModel.removeProject(indexToRemove);
+ }
+
+ menuBar: MenuBar {
+ Menu {
+ title: '&Menu'
+ Action { text: '&Settings'; onTriggered: settingsDialog.open() }
+ Action { text: '&About'; onTriggered: aboutDialog.open() }
+ MenuSeparator { }
+ Action { text: '&Quit'; onTriggered: Qt.quit() }
+ }
+ }
+
+ /*
+ All layouts and widgets try to be adaptive to variable parents, siblings, window and whatever else sizes
+ so we extensively using Grid, Column and Row layouts. The most high-level one is a composition of the list
+ and the workspace in two columns
+ */
+ GridLayout {
+ anchors.fill: parent
+ rows: 1
+ z: 2 // do not clip glow animation (see below)
+
+ ColumnLayout {
+ Layout.preferredWidth: 2.6 * parent.width / 12
+ Layout.fillHeight: true
+
+ /*
+ The dynamic list of projects (initially loaded from the QSettings, can be modified later)
+ */
+ ListView {
+ id: projectsListView
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true // crawls under the Add/Remove buttons otherwise
+
+ highlight: Rectangle { color: 'darkseagreen' }
+ highlightMoveDuration: 0 // turn off animations
+ highlightMoveVelocity: -1
+
+ model: projectsModel // backend-side
+ delegate: Component {
+ /*
+ (See setInitInfo docs) One of the two main widgets representing the project. Use Loader component
+ as it can give us the relible time of all its children loading completion (unlike Component.onCompleted)
+ */
+ Loader {
+ onLoaded: setInitInfo(index)
+ sourceComponent: RowLayout {
+ id: projectsListItem
+ property bool initloading: true // initial waiting for the backend-side
+ property bool actionRunning: false
+ property ProjectListItem project: projectsModel.getProject(index)
+ Connections {
+ target: project // (newbie hint) sender
+ // Currently, this event is equivalent to the complete initialization of the backend side of the project
+ onNameChanged: {
+ initloading = false;
+ }
+ onActionDone: {
+ actionRunning = false;
+ }
+ }
+ ColumnLayout {
+ Layout.preferredHeight: 50
+
+ Text {
+ leftPadding: 5
+ rightPadding: busy.running ? 0 : leftPadding
+ Layout.alignment: Qt.AlignBottom
+ Layout.preferredWidth: busy.running ?
+ (projectsListView.width - parent.height - leftPadding) :
+ projectsListView.width
+ elide: Text.ElideMiddle
+ maximumLineCount: 1
+ text: `${display.name}`
+ }
+ Text {
+ leftPadding: 5
+ rightPadding: busy.running ? 0 : leftPadding
+ Layout.alignment: Qt.AlignTop
+ Layout.preferredWidth: busy.running ?
+ (projectsListView.width - parent.height - leftPadding) :
+ projectsListView.width
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ text: display.current_stage
+ }
+ }
+
+ BusyIndicator {
+ id: busy
+ Layout.alignment: Qt.AlignVCenter
+ Layout.preferredWidth: parent.height
+ Layout.preferredHeight: parent.height
+ running: projectsListItem.initloading || projectsListItem.actionRunning
+ }
+
+ MouseArea {
+ x: parent.x
+ y: parent.y
+ width: parent.width
+ height: parent.height
+ enabled: !parent.initloading
+ onClicked: {
+ projectsListView.currentIndex = index;
+ projectsWorkspaceView.currentIndex = index;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ QtLabs.FolderDialog {
+ id: addProjectFolderDialog
+ currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0]
+ onAccepted: projectsModel.addProjectByPath(folder)
+ }
+ RowLayout {
+ Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter
+ Layout.fillWidth: true
+
+ Button {
+ text: 'Add'
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ display: AbstractButton.TextBesideIcon
+ icon.source: 'icons/add.svg'
+ onClicked: addProjectFolderDialog.open()
+ }
+ Button {
+ text: 'Remove'
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ display: AbstractButton.TextBesideIcon
+ icon.source: 'icons/remove.svg'
+ onClicked: moveToNextAndRemove()
+ }
+ }
+ }
+
+
+ /*
+ Main workspace. StackLayout's Repeater component seamlessly uses the same projects model (showing one -
+ current - project per screen) so all data is synchronized without any additional effort.
+ */
+ StackLayout {
+ id: projectsWorkspaceView
+ Layout.preferredWidth: 9.4 * parent.width / 12
+ Layout.fillHeight: true
+ Layout.leftMargin: 5
+ Layout.rightMargin: 10
+ Layout.topMargin: 10
+ // clip: true // do not use as it'll clip glow animation
+
+ Repeater {
+ // Use similar to ListView pattern (same projects model, Loader component)
+ model: projectsModel
+ delegate: Component {
+ Loader {
+ onLoaded: setInitInfo(index)
+ /*
+ Use another one StackLayout to separate Project initialization "screen" and Main one
+ */
+ sourceComponent: StackLayout {
+ id: mainOrInitScreen // for clarity
+ currentIndex: -1 // at widget creation we do not show main nor init screen
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ property ProjectListItem project: projectsModel.getProject(index)
+ Connections {
+ target: project // sender
+ onLogAdded: {
+ if (level === Logging.WARNING) {
+ log.append('' + message + '
');
+ } else if (level >= Logging.ERROR) {
+ log.append('' + message + '
');
+ } else {
+ log.append('
' + message + ''); + } + } + // Currently, this event is equivalent to the complete initialization of the backend side of the project + onNameChanged: { + for (let i = 0; i < buttonsModel.count; ++i) { + projActionsRow.children[i].enabled = true; + } + + const state = project.state; + const completedStages = Object.keys(state).filter(stateName => state[stateName]); + if (completedStages.length === 1 && completedStages[0] === 'EMPTY') { + initScreenLoader.active = true; + mainOrInitScreen.currentIndex = 0; // show init dialog + } else { + mainOrInitScreen.currentIndex = 1; // show main view + } + } + } + + /* + Prompt a user to perform initial setup + */ + Loader { + id: initScreenLoader + active: false + sourceComponent: Column { + Text { + text: "To complete initialization you can provide PlatformIO name of the board" + padding: 10 + } + Row { + padding: 10 + spacing: 10 + ComboBox { + id: board + editable: true + model: boardsModel // backend-side (simple string model) + textRole: 'display' + onAccepted: { + focus = false; + } + onActivated: { + focus = false; + } + onFocusChanged: { + if (!focus) { + if (find(editText) === -1) { + editText = textAt(0); // should be 'None' at index 0 + } + } else { + selectAll(); + } + } + } + /* + Trigger full run + */ + CheckBox { + id: runCheckBox + text: 'Run' + enabled: false + ToolTip { + visible: runCheckBox.hovered + Component.onCompleted: { + const actions = []; + for (let i = 3; i < buttonsModel.count; ++i) { + actions.push(`${buttonsModel.get(i).name}`); + } + text = `Do: ${actions.join(' → ')}`; + } + } + Connections { + target: board + onFocusChanged: { + if (!board.focus) { + if (board.editText === board.textAt(0)) { // should be 'None' at index 0 + runCheckBox.checked = false; + runCheckBox.enabled = false; + } else { + runCheckBox.enabled = true; + } + } + } + } + } + CheckBox { + id: openEditor + text: 'Open editor' + ToolTip { + text: 'Start the editor specified in the Settings after the completion' + visible: openEditor.hovered + } + } + } + Button { + text: 'OK' + topInset: 15 + leftInset: 10 + topPadding: 20 + leftPadding: 18 + onClicked: { + // All operations will be queued + projectsListView.currentItem.item.actionRunning = true; + + project.run('save_config', [{ + 'project': { + 'board': board.editText === board.textAt(0) ? '' : board.editText + } + }]); + + if (runCheckBox.checked) { + for (let i = 3; i < buttonsModel.count - 1; ++i) { + buttonsModel.setProperty(i, 'shouldRunNext', true); + } + projActionsRow.children[3].clicked(); + } + + if (openEditor.checked) { + if (runCheckBox.checked) { + buttonsModel.setProperty(buttonsModel.count - 1, 'shouldStartEditor', true); + } else { + projActionsRow.children[1].clicked(); + } + } + + mainOrInitScreen.currentIndex = 1; // go to main screen + initScreenLoader.sourceComponent = undefined; // destroy init screen + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + /* + Detect and reflect changes of a project outside of the app + */ + QtDialogs.MessageDialog { + // TODO: case: .ioc file can be removed on init stage too (i.e. when initDialog is active) + id: projectIncorrectDialog + text: `The project was modified outside of the stm32pio and .ioc file is no longer present.