From f81c92a72844e9eaeda7b5e2274bd8cbb7628d37 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 17 Dec 2019 23:45:47 +0300 Subject: [PATCH 01/54] * memo at setup.py * sort MANIFEST.in entries --- MANIFEST.in | 8 ++++---- TODO.md | 3 ++- setup.py | 8 ++++++++ stm32pio/tests/test.py | 1 + 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 3c52153..060a7ae 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,8 @@ +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 diff --git a/TODO.md b/TODO.md index fd0185a..f25923c 100644 --- a/TODO.md +++ b/TODO.md @@ -10,7 +10,7 @@ - [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] Upload to PyPI - [x] `__main__` - [x] Abort `--with-build` if no platformio.ini file is present - [x] Rename `stm32pio.py` -> `app.py` @@ -36,3 +36,4 @@ - [ ] 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?.. - [ ] exclude tests from the bundle (see `setup.py` options) + - [ ] generate code docs (help user to understand an internal kitchen, e.g. for embedding) diff --git a/setup.py b/setup.py index 1ed8350..005e1fe 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,11 @@ +""" +To pack: + $ python3 setup.py sdist bdist_wheel + +To upload to PyPI: + $ python3 -m twine upload dist/* +""" + import setuptools import stm32pio.app diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 445ec8a..9822af5 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -39,6 +39,7 @@ # Instantiate a temporary folder on every fixture run. It is used across all tests and is deleted on shutdown temp_dir = tempfile.TemporaryDirectory() FIXTURE_PATH = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) + print(f"The file of 'stm32pio.app' module: {STM32PIO_MAIN_SCRIPT}") print(f"Python executable: {PYTHON_EXEC} {sys.version}") print(f"Temp test fixture path: {FIXTURE_PATH}") From baab2369118ecdef6b0e1bc1b8eb8f2aed926dc7 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 18 Dec 2019 19:31:20 +0300 Subject: [PATCH 02/54] * gui initial commit (some test code from the Internet) --- stm32pio-gui/__init__.py | 0 stm32pio-gui/app.py | 52 ++++++++++++++++++ stm32pio-gui/main.qml | 111 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 stm32pio-gui/__init__.py create mode 100644 stm32pio-gui/app.py create mode 100644 stm32pio-gui/main.qml 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..f545702 --- /dev/null +++ b/stm32pio-gui/app.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# (c) EVILEG https://evileg.com/ru/post/242/ + +from PyQt5.QtGui import QGuiApplication +from PyQt5.QtQml import QQmlApplicationEngine +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot + + +class Calculator(QObject): + def __init__(self): + QObject.__init__(self) + + # cигнал передающий сумму + # обязательно даём название аргументу через arguments=['sum'] + # иначе нельзя будет его забрать в QML + sumResult = pyqtSignal(int, arguments=['sum']) + + subResult = pyqtSignal(int, arguments=['sub']) + + # слот для суммирования двух чисел + @pyqtSlot(int, int) + def sum(self, arg1, arg2): + # складываем два аргумента и испускаем сигнал + self.sumResult.emit(arg1 + arg2) + + # слот для вычитания двух чисел + @pyqtSlot(int, int) + def sub(self, arg1, arg2): + # вычитаем аргументы и испускаем сигнал + self.subResult.emit(arg1 - arg2) + + +if __name__ == "__main__": + import sys + + sys.argv += ['--style', 'material'] + + # создаём экземпляр приложения + app = QGuiApplication(sys.argv) + # создаём QML движок + engine = QQmlApplicationEngine() + # создаём объект калькулятора + calculator = Calculator() + # и регистрируем его в контексте QML + engine.rootContext().setContextProperty("calculator", calculator) + # загружаем файл qml в движок + engine.load("main.qml") + + engine.quit.connect(app.quit) + sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml new file mode 100644 index 0000000..9f7a2ff --- /dev/null +++ b/stm32pio-gui/main.qml @@ -0,0 +1,111 @@ +// latest, but native are: +// 2.5 +// 1.4 +// 1.2 +// respectively +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Controls.Material 2.13 +import QtQuick.Layouts 1.13 + +ApplicationWindow { + visible: true + width: 640 + height: 240 + title: qsTr("PyQt5 love QML") + Material.theme: Material.Light + //color: "whitesmoke" + + GridLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 9 + + columns: 4 + rows: 4 + rowSpacing: 10 + columnSpacing: 10 + + Text { + text: qsTr("First number") + } + + // Поле ввода первого числа + TextField { + id: firstNumber + } + + Text { + text: qsTr("Second number") + } + + // Поле ввода второго числа + TextField { + id: secondNumber + } + + Button { + height: 40 + Layout.fillWidth: true + highlighted: true + //Material.accent: Material.Orange + text: qsTr("Sum numbers") + + Layout.columnSpan: 2 + + onClicked: { + // Вызываем слот калькулятора, чтобы сложить числа + calculator.sum(firstNumber.text, secondNumber.text) + } + } + + Text { + text: qsTr("Result") + } + + // Здесь увидим результат сложения + Text { + id: sumResult + } + + Button { + height: 40 + Layout.fillWidth: true + text: qsTr("Subtraction numbers") + + Layout.columnSpan: 2 + + onClicked: { + // Вызываем слот калькулятора, чтобы вычесть числа + calculator.sub(firstNumber.text, secondNumber.text) + } + } + + Text { + text: qsTr("Result") + } + + // Здесь увидим результат вычитания + Text { + id: subResult + } + } + + // Здесь забираем результат сложения или вычитания чисел + Connections { + target: calculator + + // Обработчик сигнала сложения + onSumResult: { + // sum было задано через arguments=['sum'] + sumResult.text = sum + } + + // Обработчик сигнала вычитания + onSubResult: { + // sub было задано через arguments=['sub'] + subResult.text = sub + } + } +} From 1edaa180136a4d19ff78e8986ff40df13c3fcd21 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 18 Dec 2019 23:21:15 +0300 Subject: [PATCH 03/54] test on macOS --- stm32pio-gui/app.py | 2 +- stm32pio-gui/main.qml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index f545702..f5efdf1 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -35,7 +35,7 @@ def sub(self, arg1, arg2): if __name__ == "__main__": import sys - sys.argv += ['--style', 'material'] + # sys.argv += ['--style', 'material'] # создаём экземпляр приложения app = QGuiApplication(sys.argv) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 9f7a2ff..661b117 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -1,11 +1,11 @@ // latest, but native are: // 2.5 -// 1.4 +// 1.4 // 1.6 // 1.2 // respectively import QtQuick 2.13 import QtQuick.Controls 2.13 -import QtQuick.Controls.Material 2.13 +//import QtQuick.Controls.Material 2.13 import QtQuick.Layouts 1.13 ApplicationWindow { @@ -13,8 +13,8 @@ ApplicationWindow { width: 640 height: 240 title: qsTr("PyQt5 love QML") - Material.theme: Material.Light - //color: "whitesmoke" + //Material.theme: Material.Light + color: "whitesmoke" GridLayout { anchors.top: parent.top @@ -48,7 +48,7 @@ ApplicationWindow { Button { height: 40 Layout.fillWidth: true - highlighted: true + //highlighted: true //Material.accent: Material.Orange text: qsTr("Sum numbers") From 08a298072e70185dd92fb55a0be98289e084eab4 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 18 Dec 2019 23:37:19 +0300 Subject: [PATCH 04/54] test under Ubuntu, downgrade qtquick version --- stm32pio-gui/main.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 661b117..098f8e0 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -3,10 +3,10 @@ // 1.4 // 1.6 // 1.2 // respectively -import QtQuick 2.13 -import QtQuick.Controls 2.13 +import QtQuick 2.12 +import QtQuick.Controls 2.12 //import QtQuick.Controls.Material 2.13 -import QtQuick.Layouts 1.13 +import QtQuick.Layouts 1.12 ApplicationWindow { visible: true From ecfd41f41155312ef8c9bc319a4fa5c51644a3df Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 23 Dec 2019 00:50:07 +0300 Subject: [PATCH 05/54] migrate to QWidgets --- stm32pio-gui/app.py | 66 ++++++++++++------------- stm32pio-gui/main.qml | 111 ------------------------------------------ 2 files changed, 30 insertions(+), 147 deletions(-) delete mode 100644 stm32pio-gui/main.qml diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index f5efdf1..2a10348 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -1,52 +1,46 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# (c) EVILEG https://evileg.com/ru/post/242/ +import sys +from PyQt5.QtCore import QCoreApplication +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QGridLayout, QListWidget, QListWidgetItem, \ + QStackedWidget, QLabel -from PyQt5.QtGui import QGuiApplication -from PyQt5.QtQml import QQmlApplicationEngine -from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot +class CentralWidget(QWidget): + def __init__(self, parent=None): + super(CentralWidget, self).__init__(parent) + grid = QGridLayout() + self.setLayout(grid) -class Calculator(QObject): - def __init__(self): - QObject.__init__(self) + self.projects_list_widget = QListWidget() + # first = QListWidgetItem('First', self.projects_list_widget) + self.projects_list_widget.addItem('First') + self.projects_list_widget.addItem('Second') + self.projects_list_widget.addItem('Third') - # cигнал передающий сумму - # обязательно даём название аргументу через arguments=['sum'] - # иначе нельзя будет его забрать в QML - sumResult = pyqtSignal(int, arguments=['sum']) + self.project_window = QStackedWidget() + self.project_window.addWidget(QLabel('Hello, World!')) - subResult = pyqtSignal(int, arguments=['sub']) + grid.addWidget(self.projects_list_widget, 0, 0) + grid.addWidget(self.project_window, 0, 1) - # слот для суммирования двух чисел - @pyqtSlot(int, int) - def sum(self, arg1, arg2): - # складываем два аргумента и испускаем сигнал - self.sumResult.emit(arg1 + arg2) - # слот для вычитания двух чисел - @pyqtSlot(int, int) - def sub(self, arg1, arg2): - # вычитаем аргументы и испускаем сигнал - self.subResult.emit(arg1 - arg2) +class MainWindow(QMainWindow): + def __init__(self, parent=None): + super(MainWindow, self).__init__(parent) + self.setWindowTitle(QCoreApplication.applicationName()) + self.central_widget = CentralWidget() + self.setCentralWidget(self.central_widget) -if __name__ == "__main__": - import sys +if __name__ == '__main__': + QCoreApplication.setOrganizationName('ussserrr') + QCoreApplication.setApplicationName('stm32pio') - # sys.argv += ['--style', 'material'] + app = QApplication(sys.argv) - # создаём экземпляр приложения - app = QGuiApplication(sys.argv) - # создаём QML движок - engine = QQmlApplicationEngine() - # создаём объект калькулятора - calculator = Calculator() - # и регистрируем его в контексте QML - engine.rootContext().setContextProperty("calculator", calculator) - # загружаем файл qml в движок - engine.load("main.qml") + main_window = MainWindow() + main_window.show() - engine.quit.connect(app.quit) sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml deleted file mode 100644 index 098f8e0..0000000 --- a/stm32pio-gui/main.qml +++ /dev/null @@ -1,111 +0,0 @@ -// latest, but native are: -// 2.5 -// 1.4 // 1.6 -// 1.2 -// respectively -import QtQuick 2.12 -import QtQuick.Controls 2.12 -//import QtQuick.Controls.Material 2.13 -import QtQuick.Layouts 1.12 - -ApplicationWindow { - visible: true - width: 640 - height: 240 - title: qsTr("PyQt5 love QML") - //Material.theme: Material.Light - color: "whitesmoke" - - GridLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 9 - - columns: 4 - rows: 4 - rowSpacing: 10 - columnSpacing: 10 - - Text { - text: qsTr("First number") - } - - // Поле ввода первого числа - TextField { - id: firstNumber - } - - Text { - text: qsTr("Second number") - } - - // Поле ввода второго числа - TextField { - id: secondNumber - } - - Button { - height: 40 - Layout.fillWidth: true - //highlighted: true - //Material.accent: Material.Orange - text: qsTr("Sum numbers") - - Layout.columnSpan: 2 - - onClicked: { - // Вызываем слот калькулятора, чтобы сложить числа - calculator.sum(firstNumber.text, secondNumber.text) - } - } - - Text { - text: qsTr("Result") - } - - // Здесь увидим результат сложения - Text { - id: sumResult - } - - Button { - height: 40 - Layout.fillWidth: true - text: qsTr("Subtraction numbers") - - Layout.columnSpan: 2 - - onClicked: { - // Вызываем слот калькулятора, чтобы вычесть числа - calculator.sub(firstNumber.text, secondNumber.text) - } - } - - Text { - text: qsTr("Result") - } - - // Здесь увидим результат вычитания - Text { - id: subResult - } - } - - // Здесь забираем результат сложения или вычитания чисел - Connections { - target: calculator - - // Обработчик сигнала сложения - onSumResult: { - // sum было задано через arguments=['sum'] - sumResult.text = sum - } - - // Обработчик сигнала вычитания - onSubResult: { - // sub было задано через arguments=['sub'] - subResult.text = sub - } - } -} From 0ab13a206ffc8406e7cf1111549d7122dce21a1b Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 25 Dec 2019 18:17:10 +0300 Subject: [PATCH 06/54] in progress... --- stm32pio-gui/app.py | 78 +++++++++----- stm32pio-gui/main.qml | 72 +++++++++++++ .../stm32pio-test-project.ioc | 102 ------------------ stm32pio/lib.py | 72 +++++++------ 4 files changed, 159 insertions(+), 165 deletions(-) create mode 100644 stm32pio-gui/main.qml delete mode 100644 stm32pio-test-project/stm32pio-test-project.ioc diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 2a10348..bc899c0 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -2,45 +2,67 @@ # -*- coding: utf-8 -*- import sys -from PyQt5.QtCore import QCoreApplication -from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QGridLayout, QListWidget, QListWidgetItem, \ - QStackedWidget, QLabel +from PyQt5.QtCore import QCoreApplication, QUrl, QAbstractItemModel, pyqtProperty, QAbstractListModel, QModelIndex, \ + QObject, QVariant, Qt +from PyQt5.QtGui import QGuiApplication +from PyQt5.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine +from PyQt5.QtQuick import QQuickView -class CentralWidget(QWidget): - def __init__(self, parent=None): - super(CentralWidget, self).__init__(parent) - grid = QGridLayout() - self.setLayout(grid) +import stm32pio.lib - self.projects_list_widget = QListWidget() - # first = QListWidgetItem('First', self.projects_list_widget) - self.projects_list_widget.addItem('First') - self.projects_list_widget.addItem('Second') - self.projects_list_widget.addItem('Third') - self.project_window = QStackedWidget() - self.project_window.addWidget(QLabel('Hello, World!')) +class ProjectListItem(QObject): + def __init__(self, project: stm32pio.lib.Stm32pio, parent=None): + super().__init__(parent) + self.project = project - grid.addWidget(self.projects_list_widget, 0, 0) - grid.addWidget(self.project_window, 0, 1) + @pyqtProperty('QString') + def name(self): + return self.project.path.name + @pyqtProperty('QString') + def state(self): + return str(self.project.state) + + +class ProjectsList(QAbstractListModel): + def __init__(self, projects: list, parent=None): + super().__init__(parent) + self.projects = projects + + def rowCount(self, parent=None, *args, **kwargs): + return len(self.projects) + + def data(self, index: QModelIndex, role=None): + # print(index, role) + if index.row() < 0 or index.row() >= len(self.projects): + return QVariant() + else: + if role == Qt.DisplayRole: + return self.projects[index.row()] + else: + return QVariant() + + def addProject(self, project): + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.projects.append(project) + self.endInsertRows() -class MainWindow(QMainWindow): - def __init__(self, parent=None): - super(MainWindow, self).__init__(parent) - self.setWindowTitle(QCoreApplication.applicationName()) - self.central_widget = CentralWidget() - self.setCentralWidget(self.central_widget) if __name__ == '__main__': - QCoreApplication.setOrganizationName('ussserrr') - QCoreApplication.setApplicationName('stm32pio') + app = QGuiApplication(sys.argv) - app = QApplication(sys.argv) + projects = ProjectsList([]) + projects.addProject(ProjectListItem(stm32pio.lib.Stm32pio('../stm32pio-test-project', save_on_destruction=False))) + projects.addProject(ProjectListItem(stm32pio.lib.Stm32pio('../stm32pio-test-project', save_on_destruction=False))) + projects.addProject(ProjectListItem(stm32pio.lib.Stm32pio('../stm32pio-test-project', save_on_destruction=False))) - main_window = MainWindow() - main_window.show() + view = QQuickView() + view.setResizeMode(QQuickView.SizeRootObjectToView) + view.rootContext().setContextProperty('projectsModel', projects) + view.setSource(QUrl('main.qml')) + # view.show() sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml new file mode 100644 index 0000000..62ca4e7 --- /dev/null +++ b/stm32pio-gui/main.qml @@ -0,0 +1,72 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 + +ApplicationWindow { + visible: true + width: 640 + height: 480 + title: qsTr("PyQt5 love QML") + color: "whitesmoke" + + GridLayout { + columns: 2 + rows: 1 + // width: 200; height: 250 + + ListView { + width: 200; height: 250 + // anchors.fill: parent + model: projectsModel + delegate: Item { + id: projectListItem + width: ListView.view.width + height: 40 + Column { + Text { text: 'Name: ' + display.name } + Text { text: 'State: ' + display.state } + } + MouseArea { + anchors.fill: parent + onClicked: { + projectListItem.ListView.view.currentIndex = index; + view2.currentIndex = index + } + } + } + highlight: Rectangle { color: "lightsteelblue"; radius: 5 } + // focus: true + } + + SwipeView { + id: view2 + width: 200; height: 250 + // anchors.fill: parent + Repeater { + model: projectsModel + Column { + Button { + text: 'Click me' + onClicked: { + console.log('clicked'); + log.append(qsTr('SAD')); + } + } + ScrollView { + height: 100 + TextArea { + id: log + //anchors.centerIn: parent + text: 'Initial log content' + } + } + Text { + //anchors.centerIn: parent + text: 'Name: ' + display.name + } + } + } + } + } + +} diff --git a/stm32pio-test-project/stm32pio-test-project.ioc b/stm32pio-test-project/stm32pio-test-project.ioc deleted file mode 100644 index 834d9de..0000000 --- a/stm32pio-test-project/stm32pio-test-project.ioc +++ /dev/null @@ -1,102 +0,0 @@ -#MicroXplorer Configuration settings - do not modify -File.Version=6 -KeepUserPlacement=true -Mcu.Family=STM32F0 -Mcu.IP0=NVIC -Mcu.IP1=RCC -Mcu.IP2=SYS -Mcu.IP3=USART1 -Mcu.IPNb=4 -Mcu.Name=STM32F031K6Tx -Mcu.Package=LQFP32 -Mcu.Pin0=PF0-OSC_IN -Mcu.Pin1=PA2 -Mcu.Pin2=PA13 -Mcu.Pin3=PA14 -Mcu.Pin4=PA15 -Mcu.Pin5=VP_SYS_VS_Systick -Mcu.PinsNb=6 -Mcu.ThirdPartyNb=0 -Mcu.UserConstants= -Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.4.0 -MxDb.Version=DB.5.0.40 -NVIC.ForceEnableDMAVector=true -NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true -PA13.GPIOParameters=GPIO_Label -PA13.GPIO_Label=SWDIO -PA13.Locked=true -PA13.Mode=Serial_Wire -PA13.Signal=SYS_SWDIO -PA14.GPIOParameters=GPIO_Label -PA14.GPIO_Label=SWCLK -PA14.Locked=true -PA14.Mode=Serial_Wire -PA14.Signal=SYS_SWCLK -PA15.GPIOParameters=GPIO_Label -PA15.GPIO_Label=VCP_RX -PA15.Locked=true -PA15.Mode=Asynchronous -PA15.Signal=USART1_RX -PA2.GPIOParameters=GPIO_Label -PA2.GPIO_Label=VCP_TX -PA2.Locked=true -PA2.Mode=Asynchronous -PA2.Signal=USART1_TX -PCC.Checker=false -PCC.Line=STM32F0x1 -PCC.MCU=STM32F031K6Tx -PCC.PartNumber=STM32F031K6Tx -PCC.Seq0=0 -PCC.Series=STM32F0 -PCC.Temperature=25 -PCC.Vdd=3.6 -PF0-OSC_IN.Locked=true -PF0-OSC_IN.Mode=HSE-External-Clock-Source -PF0-OSC_IN.Signal=RCC_OSC_IN -PinOutPanel.RotationAngle=0 -ProjectManager.AskForMigrate=true -ProjectManager.BackupPrevious=false -ProjectManager.CompilerOptimize=6 -ProjectManager.ComputerToolchain=false -ProjectManager.CoupleFile=true -ProjectManager.CustomerFirmwarePackage= -ProjectManager.DefaultFWLocation=true -ProjectManager.DeletePrevious=true -ProjectManager.DeviceId=STM32F031K6Tx -ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 -ProjectManager.FreePins=false -ProjectManager.HalAssertFull=false -ProjectManager.HeapSize=0x200 -ProjectManager.KeepUserCode=true -ProjectManager.LastFirmware=true -ProjectManager.LibraryCopy=1 -ProjectManager.MainLocation=Src -ProjectManager.NoMain=false -ProjectManager.PreviousToolchain= -ProjectManager.ProjectBuild=false -ProjectManager.ProjectFileName=stm32pio-test-project.ioc -ProjectManager.ProjectName=stm32pio-test-project -ProjectManager.StackSize=0x400 -ProjectManager.TargetToolchain=Other Toolchains (GPDSC) -ProjectManager.ToolChainLocation= -ProjectManager.UnderRoot=false -ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true -RCC.CECFreq_Value=32786.88524590164 -RCC.FamilyName=M -RCC.HSICECFreq_Value=32786.88524590164 -RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value -RCC.PLLCLKFreq_Value=8000000 -RCC.PLLMCOFreq_Value=8000000 -RCC.TimSysFreq_Value=8000000 -RCC.VCOOutput2Freq_Value=4000000 -USART1.IPParameters=VirtualMode-Asynchronous -USART1.VirtualMode-Asynchronous=VM_ASYNC -VP_SYS_VS_Systick.Mode=SysTick -VP_SYS_VS_Systick.Signal=SYS_VS_Systick -board=NUCLEO-F031K6 -boardIOC=true diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 35c81a5..702fc99 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -100,11 +100,11 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction parameters = {} # The path is a unique identifier of the project so it would be great to remake Stm32pio class as a subclass of - # pathlib.Path and then reference it like self and not self.project_path. It is more consistent also, as now - # project_path is perceived like any other config parameter that somehow is appeared to exist outside of a - # config instance but then it will be a core identifier, a truly 'self' value. But currently pathlib.Path is not - # intended to be subclassable by-design, unfortunately. See https://bugs.python.org/issue24132 - self.project_path = self._resolve_project_path(dirty_path) + # pathlib.Path and then reference it like self and not self.path. It is more consistent also, as now path is + # perceived like any other config parameter that somehow is appeared to exist outside of a config instance but + # then it will be a core identifier, a truly 'self' value. But currently pathlib.Path is not intended to be + # subclassable by-design, unfortunately. See https://bugs.python.org/issue24132 + self.path = self._resolve_project_path(dirty_path) self.config = self._load_config_file() @@ -112,7 +112,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction self.config.set('project', 'ioc_file', str(ioc_file)) cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content')) - cubemx_script_content = cubemx_script_template.substitute(project_path=self.project_path, + cubemx_script_content = cubemx_script_template.substitute(project_path=self.path, cubemx_ioc_full_filename=self.config.get('project', 'ioc_file')) self.config.set('project', 'cubemx_script_content', cubemx_script_content) @@ -130,6 +130,10 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction self._finalizer = weakref.finalize(self, self.config.save) + def __repr__(self): + return f"Stm32pio project: {str(self.path)}" + + @property def state(self) -> ProjectState: """ @@ -140,7 +144,7 @@ def state(self) -> ProjectState: """ logger.debug("calculating the project state...") - logger.debug(f"project content: {[item.name for item in self.project_path.iterdir()]}") + logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") try: platformio_ini_is_patched = self.platformio_ini_is_patched() @@ -151,19 +155,19 @@ def state(self) -> ProjectState: # Fill the ordered dictionary with the conditions results states_conditions[ProjectState.UNDEFINED] = [True] states_conditions[ProjectState.INITIALIZED] = [ - self.project_path.joinpath(stm32pio.settings.config_file_name).is_file()] - states_conditions[ProjectState.GENERATED] = [self.project_path.joinpath('Inc').is_dir() and - len(list(self.project_path.joinpath('Inc').iterdir())) > 0, - self.project_path.joinpath('Src').is_dir() and - len(list(self.project_path.joinpath('Src').iterdir())) > 0] + self.path.joinpath(stm32pio.settings.config_file_name).is_file()] + states_conditions[ProjectState.GENERATED] = [self.path.joinpath('Inc').is_dir() and + len(list(self.path.joinpath('Inc').iterdir())) > 0, + self.path.joinpath('Src').is_dir() and + len(list(self.path.joinpath('Src').iterdir())) > 0] states_conditions[ProjectState.PIO_INITIALIZED] = [ - self.project_path.joinpath('platformio.ini').is_file() and - self.project_path.joinpath('platformio.ini').stat().st_size > 0] + self.path.joinpath('platformio.ini').is_file() and + self.path.joinpath('platformio.ini').stat().st_size > 0] states_conditions[ProjectState.PATCHED] = [ - platformio_ini_is_patched, not self.project_path.joinpath('include').is_dir()] + platformio_ini_is_patched, not self.path.joinpath('include').is_dir()] states_conditions[ProjectState.BUILT] = [ - self.project_path.joinpath('.pio').is_dir() and - any([item.is_file() for item in self.project_path.joinpath('.pio').rglob('*firmware*')])] + self.path.joinpath('.pio').is_dir() and + any([item.is_file() for item in self.path.joinpath('.pio').rglob('*firmware*')])] # Use (1,0) instead of (True,False) because on debug printing it looks better conditions_results = [] @@ -214,7 +218,7 @@ def _find_ioc_file(self) -> pathlib.Path: return ioc_file else: logger.debug("searching for any .ioc file...") - candidates = list(self.project_path.glob('*.ioc')) + candidates = list(self.path.glob('*.ioc')) if len(candidates) == 0: # good candidate for the new Python 3.8 assignment expressions feature :) raise FileNotFoundError("not found: CubeMX project .ioc file") elif len(candidates) == 1: @@ -235,14 +239,12 @@ def _load_config_file(self) -> Config: """ logger.debug(f"searching for {stm32pio.settings.config_file_name}...") - stm32pio_ini = self.project_path.joinpath(stm32pio.settings.config_file_name) - - config = Config(self.project_path, interpolation=None) + config = Config(self.path, interpolation=None) # Fill with default values config.read_dict(stm32pio.settings.config_default) # Then override by user values (if exist) - config.read(str(stm32pio_ini)) + config.read(str(self.path.joinpath(stm32pio.settings.config_file_name))) # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message @@ -353,11 +355,11 @@ def pio_init(self) -> int: logger.info("starting PlatformIO project initialization...") - platformio_ini_file = self.project_path.joinpath('platformio.ini') + platformio_ini_file = self.path.joinpath('platformio.ini') if platformio_ini_file.is_file() and platformio_ini_file.stat().st_size > 0: logger.warning("'platformio.ini' file is already exist") - command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.project_path), '-b', + command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.path), '-b', self.config.get('project', 'board'), '-O', 'framework=stm32cube'] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') @@ -388,7 +390,7 @@ def platformio_ini_is_patched(self) -> bool: platformio_ini = configparser.ConfigParser(interpolation=None) try: - if len(platformio_ini.read(self.project_path.joinpath('platformio.ini'))) == 0: + if len(platformio_ini.read(self.path.joinpath('platformio.ini'))) == 0: raise FileNotFoundError("not found: 'platformio.ini' file") except FileNotFoundError as e: raise e @@ -429,7 +431,7 @@ def patch(self) -> None: else: # Existing .ini file platformio_ini_config = configparser.ConfigParser(interpolation=None) - platformio_ini_config.read(self.project_path.joinpath('platformio.ini')) + platformio_ini_config.read(self.path.joinpath('platformio.ini')) # Our patch has the config format too patch_config = configparser.ConfigParser(interpolation=None) @@ -445,19 +447,19 @@ def patch(self) -> None: platformio_ini_config.set(patch_section, patch_key, patch_value) # Save, overwriting the original file (deletes all comments!) - with self.project_path.joinpath('platformio.ini').open(mode='w') as platformio_ini_file: + with self.path.joinpath('platformio.ini').open(mode='w') as platformio_ini_file: platformio_ini_config.write(platformio_ini_file) logger.info("'platformio.ini' has been patched") try: - shutil.rmtree(self.project_path.joinpath('include')) + shutil.rmtree(self.path.joinpath('include')) except: logger.info("cannot delete 'include' folder", exc_info=logger.getEffectiveLevel() <= logging.DEBUG) # Remove 'src' directory too but on case-sensitive file systems 'Src' == 'src' == 'SRC' so we need to check - if not self.project_path.joinpath('SRC').is_dir(): + if not self.path.joinpath('SRC').is_dir(): try: - shutil.rmtree(self.project_path.joinpath('src')) + shutil.rmtree(self.path.joinpath('src')) except: logger.info("cannot delete 'src' folder", exc_info=logger.getEffectiveLevel() <= logging.DEBUG) @@ -479,8 +481,8 @@ def start_editor(self, editor_command: str) -> int: try: # Works unstable on some Windows 7 systems, but correct on latest Win7 and Win10... - # result = subprocess.run([editor_command, str(self.project_path)], check=True) - result = subprocess.run(f"{editor_command} {str(self.project_path)}", shell=True, check=True, + # result = subprocess.run([editor_command, str(self.path)], check=True) + result = subprocess.run(f"{editor_command} {str(self.path)}", shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return result.returncode except subprocess.CalledProcessError as e: @@ -498,7 +500,7 @@ def build(self) -> int: passes a return code of the PlatformIO """ - command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.project_path)] + command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.path)] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') @@ -515,8 +517,8 @@ def clean(self) -> None: Clean-up the project folder preserving only an '.ioc' file """ - for child in self.project_path.iterdir(): - if child.name != f"{self.project_path.name}.ioc": + for child in self.path.iterdir(): + if child.name != f"{self.path.name}.ioc": if child.is_dir(): shutil.rmtree(child, ignore_errors=True) logger.debug(f"del {child}") From 46230f0ad990989b86681458fb48d483efc8e1a4 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 25 Dec 2019 18:18:16 +0300 Subject: [PATCH 07/54] add removed test project --- .../stm32pio-test-project.ioc | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 stm32pio-test-project/stm32pio-test-project.ioc diff --git a/stm32pio-test-project/stm32pio-test-project.ioc b/stm32pio-test-project/stm32pio-test-project.ioc new file mode 100644 index 0000000..834d9de --- /dev/null +++ b/stm32pio-test-project/stm32pio-test-project.ioc @@ -0,0 +1,102 @@ +#MicroXplorer Configuration settings - do not modify +File.Version=6 +KeepUserPlacement=true +Mcu.Family=STM32F0 +Mcu.IP0=NVIC +Mcu.IP1=RCC +Mcu.IP2=SYS +Mcu.IP3=USART1 +Mcu.IPNb=4 +Mcu.Name=STM32F031K6Tx +Mcu.Package=LQFP32 +Mcu.Pin0=PF0-OSC_IN +Mcu.Pin1=PA2 +Mcu.Pin2=PA13 +Mcu.Pin3=PA14 +Mcu.Pin4=PA15 +Mcu.Pin5=VP_SYS_VS_Systick +Mcu.PinsNb=6 +Mcu.ThirdPartyNb=0 +Mcu.UserConstants= +Mcu.UserName=STM32F031K6Tx +MxCube.Version=5.4.0 +MxDb.Version=DB.5.0.40 +NVIC.ForceEnableDMAVector=true +NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true +PA13.GPIOParameters=GPIO_Label +PA13.GPIO_Label=SWDIO +PA13.Locked=true +PA13.Mode=Serial_Wire +PA13.Signal=SYS_SWDIO +PA14.GPIOParameters=GPIO_Label +PA14.GPIO_Label=SWCLK +PA14.Locked=true +PA14.Mode=Serial_Wire +PA14.Signal=SYS_SWCLK +PA15.GPIOParameters=GPIO_Label +PA15.GPIO_Label=VCP_RX +PA15.Locked=true +PA15.Mode=Asynchronous +PA15.Signal=USART1_RX +PA2.GPIOParameters=GPIO_Label +PA2.GPIO_Label=VCP_TX +PA2.Locked=true +PA2.Mode=Asynchronous +PA2.Signal=USART1_TX +PCC.Checker=false +PCC.Line=STM32F0x1 +PCC.MCU=STM32F031K6Tx +PCC.PartNumber=STM32F031K6Tx +PCC.Seq0=0 +PCC.Series=STM32F0 +PCC.Temperature=25 +PCC.Vdd=3.6 +PF0-OSC_IN.Locked=true +PF0-OSC_IN.Mode=HSE-External-Clock-Source +PF0-OSC_IN.Signal=RCC_OSC_IN +PinOutPanel.RotationAngle=0 +ProjectManager.AskForMigrate=true +ProjectManager.BackupPrevious=false +ProjectManager.CompilerOptimize=6 +ProjectManager.ComputerToolchain=false +ProjectManager.CoupleFile=true +ProjectManager.CustomerFirmwarePackage= +ProjectManager.DefaultFWLocation=true +ProjectManager.DeletePrevious=true +ProjectManager.DeviceId=STM32F031K6Tx +ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 +ProjectManager.FreePins=false +ProjectManager.HalAssertFull=false +ProjectManager.HeapSize=0x200 +ProjectManager.KeepUserCode=true +ProjectManager.LastFirmware=true +ProjectManager.LibraryCopy=1 +ProjectManager.MainLocation=Src +ProjectManager.NoMain=false +ProjectManager.PreviousToolchain= +ProjectManager.ProjectBuild=false +ProjectManager.ProjectFileName=stm32pio-test-project.ioc +ProjectManager.ProjectName=stm32pio-test-project +ProjectManager.StackSize=0x400 +ProjectManager.TargetToolchain=Other Toolchains (GPDSC) +ProjectManager.ToolChainLocation= +ProjectManager.UnderRoot=false +ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true +RCC.CECFreq_Value=32786.88524590164 +RCC.FamilyName=M +RCC.HSICECFreq_Value=32786.88524590164 +RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value +RCC.PLLCLKFreq_Value=8000000 +RCC.PLLMCOFreq_Value=8000000 +RCC.TimSysFreq_Value=8000000 +RCC.VCOOutput2Freq_Value=4000000 +USART1.IPParameters=VirtualMode-Asynchronous +USART1.VirtualMode-Asynchronous=VM_ASYNC +VP_SYS_VS_Systick.Mode=SysTick +VP_SYS_VS_Systick.Signal=SYS_VS_Systick +board=NUCLEO-F031K6 +boardIOC=true From 19adb9af4f9fadcb717529cc98da97e36d5cbea6 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 25 Dec 2019 18:35:06 +0300 Subject: [PATCH 08/54] * rename Stm32pio.project_path -> Stm32pio.path * add string representation (via __repr__) --- stm32pio/lib.py | 72 +++++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 35c81a5..702fc99 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -100,11 +100,11 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction parameters = {} # The path is a unique identifier of the project so it would be great to remake Stm32pio class as a subclass of - # pathlib.Path and then reference it like self and not self.project_path. It is more consistent also, as now - # project_path is perceived like any other config parameter that somehow is appeared to exist outside of a - # config instance but then it will be a core identifier, a truly 'self' value. But currently pathlib.Path is not - # intended to be subclassable by-design, unfortunately. See https://bugs.python.org/issue24132 - self.project_path = self._resolve_project_path(dirty_path) + # pathlib.Path and then reference it like self and not self.path. It is more consistent also, as now path is + # perceived like any other config parameter that somehow is appeared to exist outside of a config instance but + # then it will be a core identifier, a truly 'self' value. But currently pathlib.Path is not intended to be + # subclassable by-design, unfortunately. See https://bugs.python.org/issue24132 + self.path = self._resolve_project_path(dirty_path) self.config = self._load_config_file() @@ -112,7 +112,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction self.config.set('project', 'ioc_file', str(ioc_file)) cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content')) - cubemx_script_content = cubemx_script_template.substitute(project_path=self.project_path, + cubemx_script_content = cubemx_script_template.substitute(project_path=self.path, cubemx_ioc_full_filename=self.config.get('project', 'ioc_file')) self.config.set('project', 'cubemx_script_content', cubemx_script_content) @@ -130,6 +130,10 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction self._finalizer = weakref.finalize(self, self.config.save) + def __repr__(self): + return f"Stm32pio project: {str(self.path)}" + + @property def state(self) -> ProjectState: """ @@ -140,7 +144,7 @@ def state(self) -> ProjectState: """ logger.debug("calculating the project state...") - logger.debug(f"project content: {[item.name for item in self.project_path.iterdir()]}") + logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") try: platformio_ini_is_patched = self.platformio_ini_is_patched() @@ -151,19 +155,19 @@ def state(self) -> ProjectState: # Fill the ordered dictionary with the conditions results states_conditions[ProjectState.UNDEFINED] = [True] states_conditions[ProjectState.INITIALIZED] = [ - self.project_path.joinpath(stm32pio.settings.config_file_name).is_file()] - states_conditions[ProjectState.GENERATED] = [self.project_path.joinpath('Inc').is_dir() and - len(list(self.project_path.joinpath('Inc').iterdir())) > 0, - self.project_path.joinpath('Src').is_dir() and - len(list(self.project_path.joinpath('Src').iterdir())) > 0] + self.path.joinpath(stm32pio.settings.config_file_name).is_file()] + states_conditions[ProjectState.GENERATED] = [self.path.joinpath('Inc').is_dir() and + len(list(self.path.joinpath('Inc').iterdir())) > 0, + self.path.joinpath('Src').is_dir() and + len(list(self.path.joinpath('Src').iterdir())) > 0] states_conditions[ProjectState.PIO_INITIALIZED] = [ - self.project_path.joinpath('platformio.ini').is_file() and - self.project_path.joinpath('platformio.ini').stat().st_size > 0] + self.path.joinpath('platformio.ini').is_file() and + self.path.joinpath('platformio.ini').stat().st_size > 0] states_conditions[ProjectState.PATCHED] = [ - platformio_ini_is_patched, not self.project_path.joinpath('include').is_dir()] + platformio_ini_is_patched, not self.path.joinpath('include').is_dir()] states_conditions[ProjectState.BUILT] = [ - self.project_path.joinpath('.pio').is_dir() and - any([item.is_file() for item in self.project_path.joinpath('.pio').rglob('*firmware*')])] + self.path.joinpath('.pio').is_dir() and + any([item.is_file() for item in self.path.joinpath('.pio').rglob('*firmware*')])] # Use (1,0) instead of (True,False) because on debug printing it looks better conditions_results = [] @@ -214,7 +218,7 @@ def _find_ioc_file(self) -> pathlib.Path: return ioc_file else: logger.debug("searching for any .ioc file...") - candidates = list(self.project_path.glob('*.ioc')) + candidates = list(self.path.glob('*.ioc')) if len(candidates) == 0: # good candidate for the new Python 3.8 assignment expressions feature :) raise FileNotFoundError("not found: CubeMX project .ioc file") elif len(candidates) == 1: @@ -235,14 +239,12 @@ def _load_config_file(self) -> Config: """ logger.debug(f"searching for {stm32pio.settings.config_file_name}...") - stm32pio_ini = self.project_path.joinpath(stm32pio.settings.config_file_name) - - config = Config(self.project_path, interpolation=None) + config = Config(self.path, interpolation=None) # Fill with default values config.read_dict(stm32pio.settings.config_default) # Then override by user values (if exist) - config.read(str(stm32pio_ini)) + config.read(str(self.path.joinpath(stm32pio.settings.config_file_name))) # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message @@ -353,11 +355,11 @@ def pio_init(self) -> int: logger.info("starting PlatformIO project initialization...") - platformio_ini_file = self.project_path.joinpath('platformio.ini') + platformio_ini_file = self.path.joinpath('platformio.ini') if platformio_ini_file.is_file() and platformio_ini_file.stat().st_size > 0: logger.warning("'platformio.ini' file is already exist") - command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.project_path), '-b', + command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.path), '-b', self.config.get('project', 'board'), '-O', 'framework=stm32cube'] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') @@ -388,7 +390,7 @@ def platformio_ini_is_patched(self) -> bool: platformio_ini = configparser.ConfigParser(interpolation=None) try: - if len(platformio_ini.read(self.project_path.joinpath('platformio.ini'))) == 0: + if len(platformio_ini.read(self.path.joinpath('platformio.ini'))) == 0: raise FileNotFoundError("not found: 'platformio.ini' file") except FileNotFoundError as e: raise e @@ -429,7 +431,7 @@ def patch(self) -> None: else: # Existing .ini file platformio_ini_config = configparser.ConfigParser(interpolation=None) - platformio_ini_config.read(self.project_path.joinpath('platformio.ini')) + platformio_ini_config.read(self.path.joinpath('platformio.ini')) # Our patch has the config format too patch_config = configparser.ConfigParser(interpolation=None) @@ -445,19 +447,19 @@ def patch(self) -> None: platformio_ini_config.set(patch_section, patch_key, patch_value) # Save, overwriting the original file (deletes all comments!) - with self.project_path.joinpath('platformio.ini').open(mode='w') as platformio_ini_file: + with self.path.joinpath('platformio.ini').open(mode='w') as platformio_ini_file: platformio_ini_config.write(platformio_ini_file) logger.info("'platformio.ini' has been patched") try: - shutil.rmtree(self.project_path.joinpath('include')) + shutil.rmtree(self.path.joinpath('include')) except: logger.info("cannot delete 'include' folder", exc_info=logger.getEffectiveLevel() <= logging.DEBUG) # Remove 'src' directory too but on case-sensitive file systems 'Src' == 'src' == 'SRC' so we need to check - if not self.project_path.joinpath('SRC').is_dir(): + if not self.path.joinpath('SRC').is_dir(): try: - shutil.rmtree(self.project_path.joinpath('src')) + shutil.rmtree(self.path.joinpath('src')) except: logger.info("cannot delete 'src' folder", exc_info=logger.getEffectiveLevel() <= logging.DEBUG) @@ -479,8 +481,8 @@ def start_editor(self, editor_command: str) -> int: try: # Works unstable on some Windows 7 systems, but correct on latest Win7 and Win10... - # result = subprocess.run([editor_command, str(self.project_path)], check=True) - result = subprocess.run(f"{editor_command} {str(self.project_path)}", shell=True, check=True, + # result = subprocess.run([editor_command, str(self.path)], check=True) + result = subprocess.run(f"{editor_command} {str(self.path)}", shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return result.returncode except subprocess.CalledProcessError as e: @@ -498,7 +500,7 @@ def build(self) -> int: passes a return code of the PlatformIO """ - command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.project_path)] + command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.path)] if logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') @@ -515,8 +517,8 @@ def clean(self) -> None: Clean-up the project folder preserving only an '.ioc' file """ - for child in self.project_path.iterdir(): - if child.name != f"{self.project_path.name}.ioc": + for child in self.path.iterdir(): + if child.name != f"{self.path.name}.ioc": if child.is_dir(): shutil.rmtree(child, ignore_errors=True) logger.debug(f"del {child}") From 8a8e54ddcc59e8deacb7a205e7fb9716e6255665 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 26 Dec 2019 12:33:55 +0300 Subject: [PATCH 09/54] add TODOs --- TODO.md | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/TODO.md b/TODO.md index f25923c..59f8eab 100644 --- a/TODO.md +++ b/TODO.md @@ -3,37 +3,13 @@ - [ ] 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 - - [x] 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) + - [x] Remove casts to string where we can use path-like objects (also 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?.. - [ ] exclude tests from the bundle (see `setup.py` options) - [ ] generate code docs (help user to understand an internal kitchen, e.g. for embedding) + - [ ] handle the project folder renaming/movement to other location and/or describe in README + - [ ] colored logs, maybe... From 11324f500032fb13f25febf95b4b74161ed06a87 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 26 Dec 2019 19:23:20 +0300 Subject: [PATCH 10/54] in progress --- stm32pio-gui/app.py | 57 ++++++++++++++++++++++++++++++++----------- stm32pio-gui/main.qml | 8 ++++-- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index bc899c0..3b87642 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -1,27 +1,39 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - +import logging import sys +import time from PyQt5.QtCore import QCoreApplication, QUrl, QAbstractItemModel, pyqtProperty, QAbstractListModel, QModelIndex, \ - QObject, QVariant, Qt + QObject, QVariant, Qt, pyqtSlot, pyqtSignal, QTimer from PyQt5.QtGui import QGuiApplication from PyQt5.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine from PyQt5.QtQuick import QQuickView import stm32pio.lib +import stm32pio.settings class ProjectListItem(QObject): + nameChanged = pyqtSignal() + def __init__(self, project: stm32pio.lib.Stm32pio, parent=None): super().__init__(parent) self.project = project + self._name = 'abc' - @pyqtProperty('QString') + @pyqtProperty(str, notify=nameChanged) def name(self): - return self.project.path.name + return self._name - @pyqtProperty('QString') + @name.setter + def name(self, value): + if self._name == value: + return + self._name = value + self.nameChanged.emit() + + @pyqtProperty(str) def state(self): return str(self.project.state) @@ -35,23 +47,33 @@ def rowCount(self, parent=None, *args, **kwargs): return len(self.projects) def data(self, index: QModelIndex, role=None): - # print(index, role) - if index.row() < 0 or index.row() >= len(self.projects): - return QVariant() - else: - if role == Qt.DisplayRole: - return self.projects[index.row()] - else: - return QVariant() + # print(index.row(), role) + if role == Qt.DisplayRole: + return self.projects[index.row()] def addProject(self, project): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.projects.append(project) self.endInsertRows() + @pyqtSlot(int, str) + def run(self, index, action): + print('index:', index, action) + time.sleep(10) + getattr(self.projects[index].project, action)() + if __name__ == '__main__': + logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance + handler = logging.StreamHandler() + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) + handler.setFormatter(logging.Formatter("%(levelname)-8s " + f"%(funcName)-{stm32pio.settings.log_function_fieldwidth}s " + "%(message)s")) + logger.debug("debug logging enabled") + app = QGuiApplication(sys.argv) projects = ProjectsList([]) @@ -59,10 +81,17 @@ def addProject(self, project): projects.addProject(ProjectListItem(stm32pio.lib.Stm32pio('../stm32pio-test-project', save_on_destruction=False))) projects.addProject(ProjectListItem(stm32pio.lib.Stm32pio('../stm32pio-test-project', save_on_destruction=False))) + # qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') + + def update_value(): + projects.projects[1].name = 'def' + timer = QTimer() + timer.timeout.connect(update_value) + timer.start(2000) + view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) view.rootContext().setContextProperty('projectsModel', projects) view.setSource(QUrl('main.qml')) - # view.show() sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 62ca4e7..b8e6ec3 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -2,6 +2,8 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +// import ProjectListItem 1.0 + ApplicationWindow { visible: true width: 640 @@ -22,7 +24,9 @@ ApplicationWindow { id: projectListItem width: ListView.view.width height: 40 + // property ProjectListItem listItem: projectsModel.getProject(index) Column { + // Text { text: listItem.name } Text { text: 'Name: ' + display.name } Text { text: 'State: ' + display.state } } @@ -48,8 +52,8 @@ ApplicationWindow { Button { text: 'Click me' onClicked: { - console.log('clicked'); - log.append(qsTr('SAD')); + // console.log('here') + projectsModel.run(index, 'clean') } } ScrollView { From 0bf7d82c519c78cce21b0dab6c424302ed5c3d44 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 27 Dec 2019 13:16:41 +0300 Subject: [PATCH 11/54] get logger in library using module __name__ --- stm32pio/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 702fc99..1e20313 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -16,7 +16,7 @@ import stm32pio.settings # Child logger, inherits parameters of the parent that has been set in more high-level code -logger = logging.getLogger('stm32pio.util') +logger = logging.getLogger(__name__) @enum.unique From 33ac7e847183cea074d2d97298f7b39ad7619320 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 27 Dec 2019 19:39:44 +0300 Subject: [PATCH 12/54] logging test --- scratch.py | 35 +++++++++++++++ stm32pio/lib.py | 110 +++++++++++++++++++++++++++--------------------- 2 files changed, 96 insertions(+), 49 deletions(-) create mode 100644 scratch.py diff --git a/scratch.py b/scratch.py new file mode 100644 index 0000000..fabb76e --- /dev/null +++ b/scratch.py @@ -0,0 +1,35 @@ +import io +import logging +# import logging.handlers +import pathlib + +import stm32pio.lib +import stm32pio.settings + + +class ProjectLogHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + print(record.args) + print('NEW MSG', self.format(record)) + +root_logger = logging.getLogger('stm32pio') +root_logger.setLevel(logging.DEBUG) + +project = stm32pio.lib.Stm32pio('C:/Users/chufyrev/Documents/GitHub/stm32pio/stm32pio-test-project', + parameters={'board': 'nucleo_f031k6'}, + save_on_destruction=False) + +# string_stream = io.StringIO() + +string_handler = ProjectLogHandler() +project.logger.addHandler(string_handler) +project.logger.setLevel(logging.DEBUG) +string_handler.setFormatter(logging.Formatter("%(levelname)-8s " + f"%(funcName)-{stm32pio.settings.log_function_fieldwidth}s " + "%(message)s")) + +project.config.save() +# print(f'the state is: {str(project.state)}') +# project.generate_code() +# print(f'the state is: {str(project.state)}') + diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 1e20313..10cc0c8 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -2,6 +2,8 @@ Main library """ +from __future__ import annotations + import collections import configparser import enum @@ -15,9 +17,6 @@ import stm32pio.settings -# Child logger, inherits parameters of the parent that has been set in more high-level code -logger = logging.getLogger(__name__) - @enum.unique class ProjectState(enum.IntEnum): @@ -43,32 +42,43 @@ class ProjectState(enum.IntEnum): PATCHED = enum.auto() BUILT = enum.auto() + def __str__(self): + string_representations = { + 'UNDEFINED': 'Undefined', + 'INITIALIZED': 'Initialized', + 'GENERATED': 'Code generated', + 'PIO_INITIALIZED': 'PlatformIO project initialized', + 'PATCHED': 'PlatformIO project patched', + 'BUILT': 'PlatformIO project built' + } + return string_representations[self.name] + class Config(configparser.ConfigParser): """ A simple subclass that has additional save() method for the better logic encapsulation """ - def __init__(self, location: pathlib.Path, *args, **kwargs): + def __init__(self, project: Stm32pio, *args, **kwargs): """ Args: location: project path (where to store the config file) *args, **kwargs: passes to the parent's constructor """ super().__init__(*args, **kwargs) - self._location = location + self.project = project def save(self) -> int: """ Tries to save the config to the file and gently log if any error occurs """ try: - with self._location.joinpath(stm32pio.settings.config_file_name).open(mode='w') as config_file: + with self.project.path.joinpath(stm32pio.settings.config_file_name).open(mode='w') as config_file: self.write(config_file) - logger.debug("stm32pio.ini config file has been saved") + self.project.logger.debug("stm32pio.ini config file has been saved") return 0 except Exception as e: - logger.warning(f"cannot save the config: {e}", exc_info=logger.getEffectiveLevel() <= logging.DEBUG) + self.project.logger.warning(f"cannot save the config: {e}", exc_info=self.project.logger.getEffectiveLevel() <= logging.DEBUG) return -1 @@ -99,6 +109,8 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction if parameters is None: parameters = {} + self.logger = logging.getLogger(f"{__name__}.{id(self)}") + # The path is a unique identifier of the project so it would be great to remake Stm32pio class as a subclass of # pathlib.Path and then reference it like self and not self.path. It is more consistent also, as now path is # perceived like any other config parameter that somehow is appeared to exist outside of a config instance but @@ -121,7 +133,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction try: board = self._resolve_board(parameters['board']) except Exception as e: - logger.warning(e) + self.logger.warning(e) self.config.set('project', 'board', board) elif self.config.get('project', 'board', fallback=None) is None: self.config.set('project', 'board', board) @@ -143,8 +155,8 @@ def state(self) -> ProjectState: enum value representing a project state """ - logger.debug("calculating the project state...") - logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") + self.logger.debug("calculating the project state...") + self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") try: platformio_ini_is_patched = self.platformio_ini_is_patched() @@ -176,10 +188,10 @@ def state(self) -> ProjectState: # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message - if logger.getEffectiveLevel() <= logging.DEBUG: + if self.logger.getEffectiveLevel() <= logging.DEBUG: states_info_str = '\n'.join( f"{state.name:20}{conditions_results[state.value - 1]}" for state in ProjectState) - logger.debug(f"determined states:\n{states_info_str}") + self.logger.debug(f"determined states:\n{states_info_str}") # Search for a consecutive sequence of 1's and find the last of them. For example, if the array is # [1,1,0,1,0,0] @@ -214,18 +226,18 @@ def _find_ioc_file(self) -> pathlib.Path: ioc_file = self.config.get('project', 'ioc_file', fallback=None) if ioc_file: ioc_file = pathlib.Path(ioc_file).resolve() - logger.debug(f"use {ioc_file.name} file from the INI config") + self.logger.debug(f"use {ioc_file.name} file from the INI config") return ioc_file else: - logger.debug("searching for any .ioc file...") + self.logger.debug("searching for any .ioc file...") candidates = list(self.path.glob('*.ioc')) if len(candidates) == 0: # good candidate for the new Python 3.8 assignment expressions feature :) raise FileNotFoundError("not found: CubeMX project .ioc file") elif len(candidates) == 1: - logger.debug(f"{candidates[0].name} is selected") + self.logger.debug(f"{candidates[0].name} is selected") return candidates[0] else: - logger.warning(f"there are multiple .ioc files, {candidates[0].name} is selected") + self.logger.warning(f"there are multiple .ioc files, {candidates[0].name} is selected") return candidates[0] @@ -238,9 +250,9 @@ def _load_config_file(self) -> Config: custom configparser.ConfigParser instance """ - logger.debug(f"searching for {stm32pio.settings.config_file_name}...") + self.logger.debug(f"searching for {stm32pio.settings.config_file_name}...") - config = Config(self.path, interpolation=None) + config = Config(self, interpolation=None) # Fill with default values config.read_dict(stm32pio.settings.config_default) # Then override by user values (if exist) @@ -248,13 +260,13 @@ def _load_config_file(self) -> Config: # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message - if logger.getEffectiveLevel() <= logging.DEBUG: + if self.logger.getEffectiveLevel() <= logging.DEBUG: debug_str = 'resolved config:' for section in config.sections(): debug_str += f"\n========== {section} ==========\n" for value in config.items(section): debug_str += f"{value}\n" - logger.debug(debug_str) + self.logger.debug(debug_str) return config @@ -288,7 +300,7 @@ def _resolve_board(self, board: str) -> str: same board that has been given if it was found, raise an exception otherwise """ - logger.debug("searching for PlatformIO board...") + self.logger.debug("searching for PlatformIO board...") result = subprocess.run([self.config.get('app', 'platformio_cmd'), 'boards'], encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Or, for Python 3.7 and above: @@ -297,7 +309,7 @@ def _resolve_board(self, board: str) -> str: if board not in result.stdout.split(): raise Exception("wrong PlatformIO STM32 board. Run 'platformio boards' for possible names") else: - logger.debug(f"PlatformIO board {board} was found") + self.logger.debug(f"PlatformIO board {board} was found") return board else: raise Exception("failed to search for PlatformIO boards") @@ -322,10 +334,10 @@ def generate_code(self) -> int: # encode since mode='w+b' cubemx_script.write(self.config.get('project', 'cubemx_script_content').encode()) - logger.info("starting to generate a code from the CubeMX .ioc file...") + self.logger.info("starting to generate a code from the CubeMX .ioc file...") command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q', cubemx_script_name, '-s'] # -q: read commands from file, -s: silent performance - if logger.getEffectiveLevel() <= logging.DEBUG: + if self.logger.getEffectiveLevel() <= logging.DEBUG: result = subprocess.run(command_arr) else: result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -337,10 +349,10 @@ def generate_code(self) -> int: pathlib.Path(cubemx_script_name).unlink() if result.returncode == 0: - logger.info("successful code generation") + self.logger.info("successful code generation") return result.returncode else: - logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" + self.logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" "Enable a verbose output or try to generate a code from the CubeMX itself.") raise Exception("code generation error") @@ -353,15 +365,15 @@ def pio_init(self) -> int: return code of the PlatformIO on success, raises an exception otherwise """ - logger.info("starting PlatformIO project initialization...") + self.logger.info("starting PlatformIO project initialization...") platformio_ini_file = self.path.joinpath('platformio.ini') if platformio_ini_file.is_file() and platformio_ini_file.stat().st_size > 0: - logger.warning("'platformio.ini' file is already exist") + self.logger.warning("'platformio.ini' file is already exist") command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.path), '-b', self.config.get('project', 'board'), '-O', 'framework=stm32cube'] - if logger.getEffectiveLevel() > logging.DEBUG: + if self.logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') result = subprocess.run(command_arr, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -371,9 +383,9 @@ def pio_init(self) -> int: # PlatformIO returns 0 even on some errors (e.g. no '--board' argument) for output in [result.stdout, result.stderr]: if 'ERROR' in output.upper(): - logger.error(output) + self.logger.error(output) raise Exception(error_msg) - logger.info("successful PlatformIO project initialization") + self.logger.info("successful PlatformIO project initialization") return result.returncode else: raise Exception(error_msg) @@ -408,11 +420,11 @@ def platformio_ini_is_patched(self) -> bool: for patch_key, patch_value in patch_config.items(patch_section): platformio_ini_value = platformio_ini.get(patch_section, patch_key, fallback=None) if platformio_ini_value != patch_value: - logger.debug(f"[{patch_section}]{patch_key}: patch value is\n{patch_value}\nbut " + self.logger.debug(f"[{patch_section}]{patch_key}: patch value is\n{patch_value}\nbut " f"platformio.ini contains\n{platformio_ini_value}") return False else: - logger.debug(f"platformio.ini has not {patch_section} section") + self.logger.debug(f"platformio.ini has not {patch_section} section") return False return True @@ -424,10 +436,10 @@ def patch(self) -> None: will be lost at this stage. Also, the order may be violated. In the end, remove old empty folders """ - logger.debug("patching 'platformio.ini' file...") + self.logger.debug("patching 'platformio.ini' file...") if self.platformio_ini_is_patched(): - logger.info("'platformio.ini' has been already patched") + self.logger.info("'platformio.ini' has been already patched") else: # Existing .ini file platformio_ini_config = configparser.ConfigParser(interpolation=None) @@ -440,28 +452,28 @@ def patch(self) -> None: # Merge 2 configs for patch_section in patch_config.sections(): if not platformio_ini_config.has_section(patch_section): - logger.debug(f"[{patch_section}] section was added") + self.logger.debug(f"[{patch_section}] section was added") platformio_ini_config.add_section(patch_section) for patch_key, patch_value in patch_config.items(patch_section): - logger.debug(f"set [{patch_section}]{patch_key} = {patch_value}") + self.logger.debug(f"set [{patch_section}]{patch_key} = {patch_value}") platformio_ini_config.set(patch_section, patch_key, patch_value) # Save, overwriting the original file (deletes all comments!) with self.path.joinpath('platformio.ini').open(mode='w') as platformio_ini_file: platformio_ini_config.write(platformio_ini_file) - logger.info("'platformio.ini' has been patched") + self.logger.info("'platformio.ini' has been patched") try: shutil.rmtree(self.path.joinpath('include')) except: - logger.info("cannot delete 'include' folder", exc_info=logger.getEffectiveLevel() <= logging.DEBUG) + self.logger.info("cannot delete 'include' folder", exc_info=self.logger.getEffectiveLevel() <= logging.DEBUG) # Remove 'src' directory too but on case-sensitive file systems 'Src' == 'src' == 'SRC' so we need to check if not self.path.joinpath('SRC').is_dir(): try: shutil.rmtree(self.path.joinpath('src')) except: - logger.info("cannot delete 'src' folder", exc_info=logger.getEffectiveLevel() <= logging.DEBUG) + self.logger.info("cannot delete 'src' folder", exc_info=self.logger.getEffectiveLevel() <= logging.DEBUG) def start_editor(self, editor_command: str) -> int: @@ -477,7 +489,7 @@ def start_editor(self, editor_command: str) -> int: passes a return code of the command """ - logger.info(f"starting an editor '{editor_command}'...") + self.logger.info(f"starting an editor '{editor_command}'...") try: # Works unstable on some Windows 7 systems, but correct on latest Win7 and Win10... @@ -487,7 +499,7 @@ def start_editor(self, editor_command: str) -> int: return result.returncode except subprocess.CalledProcessError as e: output = e.stdout if e.stderr is None else e.stderr - logger.error(f"failed to start the editor {editor_command}: {output}") + self.logger.error(f"failed to start the editor {editor_command}: {output}") return e.returncode @@ -501,14 +513,14 @@ def build(self) -> int: """ command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.path)] - if logger.getEffectiveLevel() > logging.DEBUG: + if self.logger.getEffectiveLevel() > logging.DEBUG: command_arr.append('--silent') result = subprocess.run(command_arr) if result.returncode == 0: - logger.info("successful PlatformIO build") + self.logger.info("successful PlatformIO build") else: - logger.error("PlatformIO build error") + self.logger.error("PlatformIO build error") return result.returncode @@ -521,9 +533,9 @@ def clean(self) -> None: if child.name != f"{self.path.name}.ioc": if child.is_dir(): shutil.rmtree(child, ignore_errors=True) - logger.debug(f"del {child}") + self.logger.debug(f"del {child}") elif child.is_file(): child.unlink() - logger.debug(f"del {child}") + self.logger.debug(f"del {child}") - logger.info("project has been cleaned") + self.logger.info("project has been cleaned") From 09c9945e4e3d38f5588d76bf764cd64156d8128d Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 30 Dec 2019 00:26:29 +0300 Subject: [PATCH 13/54] new logging system done, beta test --- README.md | 2 +- TODO.md | 4 ++ scratch.py | 41 +++++++----- scratch2.py | 141 +++++++++++++++++++++++++++++++++++++++++ stm32pio/app.py | 17 +++-- stm32pio/lib.py | 52 +++++++-------- stm32pio/settings.py | 2 +- stm32pio/tests/test.py | 17 +++-- stm32pio/util.py | 72 +++++++++++++++++++++ 9 files changed, 290 insertions(+), 58 deletions(-) create mode 100644 scratch2.py create mode 100644 stm32pio/util.py diff --git a/README.md b/README.md index 715a31f..dd1511e 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ 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 to fail. For specific test suite or case you can use ```shell script diff --git a/TODO.md b/TODO.md index 59f8eab..9b1168f 100644 --- a/TODO.md +++ b/TODO.md @@ -13,3 +13,7 @@ - [ ] generate code docs (help user to understand an internal kitchen, e.g. for embedding) - [ ] handle the project folder renaming/movement to other location and/or describe in README - [ ] colored logs, maybe... + - [ ] check logging work when embed stm32pio lib in third-party stuff + - [ ] logging process coverage in README + - [ ] merge subprocess pipes to one where suitable + - [ ] redirect subprocess pipes to DEVNULL where suitable to suppress output diff --git a/scratch.py b/scratch.py index fabb76e..3bb14ef 100644 --- a/scratch.py +++ b/scratch.py @@ -1,7 +1,4 @@ -import io import logging -# import logging.handlers -import pathlib import stm32pio.lib import stm32pio.settings @@ -12,24 +9,34 @@ def emit(self, record: logging.LogRecord) -> None: print(record.args) print('NEW MSG', self.format(record)) -root_logger = logging.getLogger('stm32pio') -root_logger.setLevel(logging.DEBUG) +# root_logger = logging.getLogger('stm32pio') +# root_logger.setLevel(logging.DEBUG) -project = stm32pio.lib.Stm32pio('C:/Users/chufyrev/Documents/GitHub/stm32pio/stm32pio-test-project', - parameters={'board': 'nucleo_f031k6'}, - save_on_destruction=False) +# project = stm32pio.lib.Stm32pio('C:/Users/chufyrev/Documents/GitHub/stm32pio/stm32pio-test-project', +# parameters={'board': 'nucleo_f031k6'}, +# save_on_destruction=False) -# string_stream = io.StringIO() +# string_handler = ProjectLogHandler() +# project.logger.addHandler(string_handler) +# project.logger.setLevel(logging.DEBUG) +# string_handler.setFormatter(logging.Formatter("%(levelname)-8s " +# f"%(funcName)-{stm32pio.settings.log_fieldwidth_function}s " +# "%(message)s")) -string_handler = ProjectLogHandler() -project.logger.addHandler(string_handler) -project.logger.setLevel(logging.DEBUG) -string_handler.setFormatter(logging.Formatter("%(levelname)-8s " - f"%(funcName)-{stm32pio.settings.log_function_fieldwidth}s " - "%(message)s")) - -project.config.save() +# project.config.save() # print(f'the state is: {str(project.state)}') # project.generate_code() # print(f'the state is: {str(project.state)}') + +class Base: + def __init__(self): + print('base: ', id(self)) + +class Child(Base): + def __init__(self): + print('child 1:', id(self)) + super().__init__() + print('child 2:', id(self)) + +inst = Child() diff --git a/scratch2.py b/scratch2.py new file mode 100644 index 0000000..098d4b6 --- /dev/null +++ b/scratch2.py @@ -0,0 +1,141 @@ +import logging +import threading +import os +import subprocess + +# logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) + +class LogPipe(threading.Thread): + + def __init__(self, logger: logging.Logger, level: int, *args, **kwargs): + """Setup the object with a logger and a loglevel + and start the thread + """ + super().__init__(*args, **kwargs) + + self.logger = logger + self.level = level + + self.fd_read, self.fd_write = os.pipe() + self.pipe_reader = os.fdopen(self.fd_read) + + def __enter__(self): + self.start() + return self.fd_write + + def run(self): + """Run the thread, logging everything. + """ + for line in iter(self.pipe_reader.readline, ''): + self.logger.log(self.level, line.strip('\n'), 'from_subprocess') + + self.pipe_reader.close() + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + The exception will be passed forward, if present so we don't need to do something with that. The tear-down + process will be done anyway + """ + os.close(self.fd_write) + + +# For testing +# if __name__ == "__main__": +# import sys +# +# logpipe = LogPipe() +# with subprocess.Popen(['curl', 'www.google.com'], stdout=logpipe, stderr=logpipe) as s: +# logpipe.close() +# +# sys.exit() + + + +old_factory = logging.getLogRecordFactory() +def record_factory(*log_record_args, **kwargs): + args_idx = 5 + if 'from_subprocess' in log_record_args[args_idx]: + new_log_record_args = log_record_args[:args_idx] + (tuple(arg for arg in log_record_args[args_idx] if arg != 'from_subprocess'),) + log_record_args[args_idx+1:] + record = old_factory(*new_log_record_args, **kwargs) + record.from_subprocess = True + else: + record = old_factory(*log_record_args, **kwargs) + return record +logging.setLogRecordFactory(record_factory) + + +class DispatchingFormatter(logging.Formatter): + def __init__(self, *args, custom_formatters=None, **kwargs): + self._formatters = custom_formatters + super().__init__(*args, **kwargs) + + def format(self, record): + if hasattr(record, 'from_subprocess') and record.from_subprocess: + try: + return self._formatters['subprocess'].format(record) + except AttributeError: + pass + return super().format(record) + + +root_l = logging.getLogger('root') +root_f = DispatchingFormatter("%(levelname)-8s %(funcName)-26s %(message)s", + custom_formatters={ + 'subprocess': logging.Formatter('ONLY MESSAGE %(message)s') + } +) +# root_f = logging.Formatter("USER HASN'T USED DispatchingFormatter %(message)s") +root_h = logging.StreamHandler() +root_h.setFormatter(root_f) +root_l.addHandler(root_h) +root_l.setLevel(logging.INFO) +# root_h.setLevel(logging.DEBUG) + + +child_l = logging.getLogger('root.child') +# child_f = DispatchingFormatter("%(levelname)-8s %(funcName)-26s %(message)s", +# custom_formatters={ +# 'subprocess': logging.Formatter('ONLY MESSAGE %(message)s') +# } +# ) +# root_f = logging.Formatter("USER HASN'T USED DispatchingFormatter %(message)s") +# child_h = logging.StreamHandler() +# child_h.setFormatter(child_f) +# child_l.addHandler(child_h) +# child_l.setLevel(logging.DEBUG) +# child_h.setLevel(logging.DEBUG) + +instance_l = logging.getLogger('root.child.instance') + + +root_l.info('Hello from root') +child_l.info('Hello from child') +instance_l.info('Hello from instance') +instance_l.info('Hello from instance but from subprocess', 'from_subprocess') + + +# logpipe = LogPipe(child_l) +# subprocess.run(['curl', 'www.google.com'], stdout=logpipe, stderr=logpipe) +# logpipe.close() + +try: + with LogPipe(instance_l, logging.DEBUG) as logpipe: + subprocess.run(['curl', 'www.google.com'], stdout=logpipe, stderr=logpipe) + # subprocess.run(['blabla'], stdout=logpipe, stderr=logpipe) + print('not called cause the exception') +except FileNotFoundError: + print('exception here') + +# with subprocess.Popen(['curl', 'www.google.com'], stdout=logpipe, stderr=logpipe) as s: +# logpipe.close() + +# popen = subprocess.Popen( +# ['curl', 'www.google.com'], +# stdout=subprocess.PIPE, +# stderr=subprocess.STDOUT, +# universal_newlines=True +# ) +# for stdout_line in iter(popen.stdout.readline, ""): +# instance_l.debug(stdout_line.strip('\n'), 'from_subprocess') +# popen.stdout.close() +# return_code = popen.wait() diff --git a/stm32pio/app.py b/stm32pio/app.py index bff187f..a777fe5 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -75,31 +75,34 @@ def main(sys_argv=None) -> int: if sys_argv is None: sys_argv = sys.argv[1:] + # import modules after sys.path modification import stm32pio.settings + import stm32pio.lib + import stm32pio.util args = parse_args(sys_argv) logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance handler = logging.StreamHandler() logger.addHandler(handler) + special_formatters = { 'subprocess': logging.Formatter('%(message)s') } # Currently only 2 levels of verbosity through the '-v' option are counted (INFO (default) and DEBUG (-v)) if args is not None and args.subcommand is not None and args.verbose: logger.setLevel(logging.DEBUG) - handler.setFormatter(logging.Formatter("%(levelname)-8s " - f"%(funcName)-{stm32pio.settings.log_function_fieldwidth}s " - "%(message)s")) + handler.setFormatter(stm32pio.util.DispatchingFormatter( + f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", + special=special_formatters)) logger.debug("debug logging enabled") elif args is not None and args.subcommand is not None: logger.setLevel(logging.INFO) - handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s")) + handler.setFormatter(stm32pio.util.DispatchingFormatter("%(levelname)-8s %(message)s", + special=special_formatters)) else: logger.setLevel(logging.INFO) handler.setFormatter(logging.Formatter("%(message)s")) logger.info("\nNo arguments were given, exiting...") return 0 - import stm32pio.lib # import the module after sys.path modification and logger configuration - # Main routine try: if args.subcommand == 'init': @@ -137,7 +140,7 @@ def main(sys_argv=None) -> int: # library is designed to throw the exception in bad cases so we catch here globally except Exception as e: - logger.exception(e, exc_info=logger.getEffectiveLevel() <= logging.DEBUG) + logger.exception(e, exc_info=logger.isEnabledFor(logging.DEBUG)) return -1 logger.info("exiting...") diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 10cc0c8..34aa752 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -16,6 +16,7 @@ import weakref import stm32pio.settings +import stm32pio.util @enum.unique @@ -78,7 +79,7 @@ def save(self) -> int: self.project.logger.debug("stm32pio.ini config file has been saved") return 0 except Exception as e: - self.project.logger.warning(f"cannot save the config: {e}", exc_info=self.project.logger.getEffectiveLevel() <= logging.DEBUG) + self.project.logger.warning(f"cannot save the config: {e}", exc_info=self.project.logger.isEnabledFor(logging.DEBUG)) return -1 @@ -188,7 +189,7 @@ def state(self) -> ProjectState: # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message - if self.logger.getEffectiveLevel() <= logging.DEBUG: + if self.logger.isEnabledFor(logging.DEBUG): states_info_str = '\n'.join( f"{state.name:20}{conditions_results[state.value - 1]}" for state in ProjectState) self.logger.debug(f"determined states:\n{states_info_str}") @@ -260,7 +261,7 @@ def _load_config_file(self) -> Config: # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message - if self.logger.getEffectiveLevel() <= logging.DEBUG: + if self.logger.isEnabledFor(logging.DEBUG): debug_str = 'resolved config:' for section in config.sections(): debug_str += f"\n========== {section} ==========\n" @@ -291,7 +292,8 @@ def _resolve_project_path(dirty_path: str) -> pathlib.Path: def _resolve_board(self, board: str) -> str: """ - Check if given board is a correct board name in the PlatformIO database + Check if given board is a correct board name in the PlatformIO database. Simply get the whole list of all boards + using CLI command and search in the STDOUT Args: board: string representing PlatformIO board name (for example, 'nucleo_f031k6') @@ -337,12 +339,8 @@ def generate_code(self) -> int: self.logger.info("starting to generate a code from the CubeMX .ioc file...") command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q', cubemx_script_name, '-s'] # -q: read commands from file, -s: silent performance - if self.logger.getEffectiveLevel() <= logging.DEBUG: - result = subprocess.run(command_arr) - else: - result = subprocess.run(command_arr, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Or, for Python 3.7 and above: - # result = subprocess.run(command_arr, capture_output=True) + with stm32pio.util.LogPipe(self.logger, logging.DEBUG) as log_pipe: + result = subprocess.run(command_arr, stdout=log_pipe, stderr=log_pipe) except Exception as e: raise e # re-raise an exception after the final block finally: @@ -353,7 +351,7 @@ def generate_code(self) -> int: return result.returncode else: self.logger.error(f"code generation error (CubeMX return code is {result.returncode}).\n" - "Enable a verbose output or try to generate a code from the CubeMX itself.") + "Enable a verbose output or try to generate a code from the CubeMX itself.") raise Exception("code generation error") def pio_init(self) -> int: @@ -373,21 +371,22 @@ def pio_init(self) -> int: command_arr = [self.config.get('app', 'platformio_cmd'), 'init', '-d', str(self.path), '-b', self.config.get('project', 'board'), '-O', 'framework=stm32cube'] - if self.logger.getEffectiveLevel() > logging.DEBUG: + if not self.logger.isEnabledFor(logging.DEBUG): command_arr.append('--silent') - result = subprocess.run(command_arr, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run(command_arr, encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.STDOUT) error_msg = "PlatformIO project initialization error" if result.returncode == 0: # PlatformIO returns 0 even on some errors (e.g. no '--board' argument) - for output in [result.stdout, result.stderr]: - if 'ERROR' in output.upper(): - self.logger.error(output) - raise Exception(error_msg) + if 'ERROR' in result.stdout: + self.logger.error(result.stdout) + raise Exception(error_msg) + self.logger.debug(result.stdout, 'from_subprocess') self.logger.info("successful PlatformIO project initialization") return result.returncode else: + self.logger.error(result.stdout) raise Exception(error_msg) @@ -421,7 +420,7 @@ def platformio_ini_is_patched(self) -> bool: platformio_ini_value = platformio_ini.get(patch_section, patch_key, fallback=None) if platformio_ini_value != patch_value: self.logger.debug(f"[{patch_section}]{patch_key}: patch value is\n{patch_value}\nbut " - f"platformio.ini contains\n{platformio_ini_value}") + f"platformio.ini contains\n{platformio_ini_value}") return False else: self.logger.debug(f"platformio.ini has not {patch_section} section") @@ -467,13 +466,13 @@ def patch(self) -> None: try: shutil.rmtree(self.path.joinpath('include')) except: - self.logger.info("cannot delete 'include' folder", exc_info=self.logger.getEffectiveLevel() <= logging.DEBUG) + self.logger.info("cannot delete 'include' folder", exc_info=self.logger.isEnabledFor(logging.DEBUG)) # Remove 'src' directory too but on case-sensitive file systems 'Src' == 'src' == 'SRC' so we need to check if not self.path.joinpath('SRC').is_dir(): try: shutil.rmtree(self.path.joinpath('src')) except: - self.logger.info("cannot delete 'src' folder", exc_info=self.logger.getEffectiveLevel() <= logging.DEBUG) + self.logger.info("cannot delete 'src' folder", exc_info=self.logger.isEnabledFor(logging.DEBUG)) def start_editor(self, editor_command: str) -> int: @@ -495,11 +494,11 @@ def start_editor(self, editor_command: str) -> int: # Works unstable on some Windows 7 systems, but correct on latest Win7 and Win10... # result = subprocess.run([editor_command, str(self.path)], check=True) result = subprocess.run(f"{editor_command} {str(self.path)}", shell=True, check=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + self.logger.debug(result.stdout, 'from_subprocess') return result.returncode except subprocess.CalledProcessError as e: - output = e.stdout if e.stderr is None else e.stderr - self.logger.error(f"failed to start the editor {editor_command}: {output}") + self.logger.error(f"failed to start the editor {editor_command}: {e.stdout}") return e.returncode @@ -512,11 +511,14 @@ def build(self) -> int: passes a return code of the PlatformIO """ + self.logger.info("starting PlatformIO project build...") + command_arr = [self.config.get('app', 'platformio_cmd'), 'run', '-d', str(self.path)] - if self.logger.getEffectiveLevel() > logging.DEBUG: + if not self.logger.isEnabledFor(logging.DEBUG): command_arr.append('--silent') - result = subprocess.run(command_arr) + with stm32pio.util.LogPipe(self.logger, logging.DEBUG) as log_pipe: + result = subprocess.run(command_arr, stdout=log_pipe, stderr=log_pipe) if result.returncode == 0: self.logger.info("successful PlatformIO build") else: diff --git a/stm32pio/settings.py b/stm32pio/settings.py index 9e54f0c..2de1d6e 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -44,4 +44,4 @@ config_file_name = 'stm32pio.ini' -log_function_fieldwidth = 26 +log_fieldwidth_function = 26 diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 9822af5..6dbafc1 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -1,4 +1,6 @@ """ +Make sure the test project tree is clean before running the tests + 'pyenv' was used to perform tests with different Python versions under Ubuntu: https://www.tecmint.com/pyenv-install-and-manage-multiple-python-versions-in-linux/ @@ -43,6 +45,7 @@ print(f"The file of 'stm32pio.app' module: {STM32PIO_MAIN_SCRIPT}") print(f"Python executable: {PYTHON_EXEC} {sys.version}") print(f"Temp test fixture path: {FIXTURE_PATH}") +print() class CustomTestCase(unittest.TestCase): @@ -481,12 +484,13 @@ def test_verbose(self): msg="Verbose logging output hasn't been enabled on stderr") # Inject all methods' names in the regex. Inject the width of field in a log format string regex = re.compile("^(?=(DEBUG) {0,4})(?=.{8} (?=(" + '|'.join(methods) + ") {0," + - str(stm32pio.settings.log_function_fieldwidth) + "})(?=.{" + - str(stm32pio.settings.log_function_fieldwidth) + "} [^ ]))", + str(stm32pio.settings.log_fieldwidth_function) + "})(?=.{" + + str(stm32pio.settings.log_fieldwidth_function) + "} [^ ]))", flags=re.MULTILINE) self.assertGreaterEqual(len(re.findall(regex, result.stderr)), 1, msg="Logs messages doesn't match the format") - self.assertIn('Starting STM32CubeMX', result.stdout, msg="STM32CubeMX didn't print its logs") + self.assertEqual(len(result.stdout), 0, msg="Process has printed something directly into STDOUT bypassing " + "logging") def test_non_verbose(self): """ @@ -507,17 +511,16 @@ def test_non_verbose(self): self.assertNotIn('DEBUG', result.stdout, msg="Verbose logging output has been enabled on stdout") regex = re.compile("^(?=(INFO) {0,4})(?=.{8} ((?!( |" + '|'.join(methods) + "))))", flags=re.MULTILINE) - self.assertGreaterEqual(len(re.findall(regex, result.stderr)), 1, - msg="Logs messages doesn't match the format") + self.assertGreaterEqual(len(re.findall(regex, result.stderr)), 1, msg="Logs messages doesn't match the format") - self.assertNotIn('Starting STM32CubeMX', result.stdout, msg="STM32CubeMX printed its logs") + self.assertNotIn('Starting STM32CubeMX', result.stderr, msg="STM32CubeMX printed its logs") def test_init(self): """ Check for config creation and parameters presence """ result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'init', '-d', str(FIXTURE_PATH), - '-b', TEST_PROJECT_BOARD]) + '-b', TEST_PROJECT_BOARD], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) self.assertEqual(result.returncode, 0, msg="Non-zero return code") self.assertTrue(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).is_file(), diff --git a/stm32pio/util.py b/stm32pio/util.py new file mode 100644 index 0000000..2aeecce --- /dev/null +++ b/stm32pio/util.py @@ -0,0 +1,72 @@ +import logging +import os +import threading + + +module_logger = logging.getLogger(__name__) + +old_log_record_factory = logging.getLogRecordFactory() +def log_record_factory(*log_record_args, **log_record_kwargs): + args_idx = 5 + if 'from_subprocess' in log_record_args[args_idx]: + new_log_record_args = log_record_args[:args_idx] +\ + (tuple(arg for arg in log_record_args[args_idx] if arg != 'from_subprocess'),) +\ + log_record_args[args_idx + 1:] + record = old_log_record_factory(*new_log_record_args, **log_record_kwargs) + record.from_subprocess = True + else: + record = old_log_record_factory(*log_record_args, **log_record_kwargs) + return record +logging.setLogRecordFactory(log_record_factory) + +class DispatchingFormatter(logging.Formatter): + def __init__(self, *args, special: dict = None, **kwargs): + if isinstance(special, dict) and all(isinstance(formatter, logging.Formatter) for formatter in special.values()): + self._formatters = special + else: + module_logger.warning(f"'special' argument is for providing custom formatters for special logging events " + "and should be a dictionary with logging.Formatter values") + self._formatters = {} + super().__init__(*args, **kwargs) + + def format(self, record): + if hasattr(record, 'from_subprocess') and record.from_subprocess: + try: + return self._formatters['subprocess'].format(record) + except AttributeError: + pass + return super().format(record) + + +class LogPipe(threading.Thread): + + def __init__(self, logger: logging.Logger, level: int, *args, **kwargs): + """Setup the object with a logger and a loglevel + and start the thread + """ + super().__init__(*args, **kwargs) + + self.logger = logger + self.level = level + + self.fd_read, self.fd_write = os.pipe() + self.pipe_reader = os.fdopen(self.fd_read) + + def __enter__(self): + self.start() + return self.fd_write + + def run(self): + """Run the thread, logging everything. + """ + for line in iter(self.pipe_reader.readline, ''): + self.logger.log(self.level, line.strip('\n'), 'from_subprocess') + + self.pipe_reader.close() + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + The exception will be passed forward, if present, so we don't need to do something with that. The tear-down + process will be done anyway + """ + os.close(self.fd_write) From d978ad229568976c38e856af73a0c5f28f65a9f7 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 30 Dec 2019 20:45:58 +0300 Subject: [PATCH 14/54] try to pass logs to QML --- stm32pio-gui/app.py | 134 +++++++++++++++++++++++++++--------------- stm32pio-gui/main.qml | 51 +++++++++++----- stm32pio/app.py | 4 +- 3 files changed, 123 insertions(+), 66 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 3b87642..de74ce8 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -1,41 +1,65 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + import logging import sys import time -from PyQt5.QtCore import QCoreApplication, QUrl, QAbstractItemModel, pyqtProperty, QAbstractListModel, QModelIndex, \ - QObject, QVariant, Qt, pyqtSlot, pyqtSignal, QTimer -from PyQt5.QtGui import QGuiApplication -from PyQt5.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine -from PyQt5.QtQuick import QQuickView +from PySide2.QtCore import QCoreApplication, QUrl, QAbstractItemModel, Property, QAbstractListModel, QModelIndex, \ + QObject, Qt, Slot, Signal, QTimer +from PySide2.QtGui import QGuiApplication +from PySide2.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine +from PySide2.QtQuick import QQuickView -import stm32pio.lib import stm32pio.settings +import stm32pio.lib +import stm32pio.util -class ProjectListItem(QObject): - nameChanged = pyqtSignal() - def __init__(self, project: stm32pio.lib.Stm32pio, parent=None): - super().__init__(parent) - self.project = project - self._name = 'abc' +special_formatters = {'subprocess': logging.Formatter('%(message)s')} + + +class ProjectListItem(stm32pio.lib.Stm32pio, QObject): + # nameChanged = Signal() + logAdded = Signal(str, arguments=['message']) + + def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, qt_parent=None): + QObject.__init__(self, parent=qt_parent) + + # this = self + class InternalHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + print(record) + # this.log(self.format(record)) + self.handler = InternalHandler() + logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") + logger.addHandler(self.handler) + logger.setLevel(logging.DEBUG) + self.handler.setFormatter(stm32pio.util.DispatchingFormatter( + f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", + special=special_formatters)) - @pyqtProperty(str, notify=nameChanged) + stm32pio.lib.Stm32pio.__init__(self, dirty_path, parameters=parameters, save_on_destruction=save_on_destruction) + self._name = self.path.name + + # def log(self, message): + # self.logAdded.emit(message) + + @Property(str) def name(self): return self._name - @name.setter - def name(self, value): - if self._name == value: - return - self._name = value - self.nameChanged.emit() + # @name.setter + # def name(self, value): + # if self._name == value: + # return + # self._name = value + # self.nameChanged.emit() - @pyqtProperty(str) + @Property(str) def state(self): - return str(self.project.state) + return str(super().state) class ProjectsList(QAbstractListModel): @@ -43,6 +67,11 @@ def __init__(self, projects: list, parent=None): super().__init__(parent) self.projects = projects + # @Slot(int) + # def getProject(self, index): + # print(index) + # return self.projects[index] + def rowCount(self, parent=None, *args, **kwargs): return len(self.projects) @@ -56,42 +85,51 @@ def addProject(self, project): self.projects.append(project) self.endInsertRows() - @pyqtSlot(int, str) + @Slot(int, str) def run(self, index, action): print('index:', index, action) - time.sleep(10) - getattr(self.projects[index].project, action)() + # time.sleep(10) + getattr(self.projects[index], action)() if __name__ == '__main__': - logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance - handler = logging.StreamHandler() - logger.addHandler(handler) - logger.setLevel(logging.DEBUG) - handler.setFormatter(logging.Formatter("%(levelname)-8s " - f"%(funcName)-{stm32pio.settings.log_function_fieldwidth}s " - "%(message)s")) - logger.debug("debug logging enabled") + # logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance + # handler = InternalHandler() + # logger.addHandler(handler) + # special_formatters = {'subprocess': logging.Formatter('%(message)s')} + # logger.setLevel(logging.DEBUG) + # handler.setFormatter(stm32pio.util.DispatchingFormatter( + # f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", + # special=special_formatters)) + # logger.debug("debug logging enabled") app = QGuiApplication(sys.argv) - projects = ProjectsList([]) - projects.addProject(ProjectListItem(stm32pio.lib.Stm32pio('../stm32pio-test-project', save_on_destruction=False))) - projects.addProject(ProjectListItem(stm32pio.lib.Stm32pio('../stm32pio-test-project', save_on_destruction=False))) - projects.addProject(ProjectListItem(stm32pio.lib.Stm32pio('../stm32pio-test-project', save_on_destruction=False))) + engine = QQmlApplicationEngine() - # qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') - - def update_value(): - projects.projects[1].name = 'def' - timer = QTimer() - timer.timeout.connect(update_value) - timer.start(2000) - - view = QQuickView() - view.setResizeMode(QQuickView.SizeRootObjectToView) - view.rootContext().setContextProperty('projectsModel', projects) - view.setSource(QUrl('main.qml')) + # print('AAAAAAAAAAA', qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem')) + projects = ProjectsList([]) + projects.addProject(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) + projects.addProject(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) + projects.addProject(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) + + # def update_value(): + # projects.projects[1].name = 'def' + # timer = QTimer() + # timer.timeout.connect(update_value) + # timer.start(2000) + + # First approach + engine.rootContext().setContextProperty("projectsModel", projects) + engine.load(QUrl('main.qml')) + + # Second approach + # view = QQuickView() + # view.setResizeMode(QQuickView.SizeRootObjectToView) + # view.rootContext().setContextProperty('projectsModel', projects) + # view.setSource(QUrl('main.qml')) + + engine.quit.connect(app.quit) sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index b8e6ec3..e8100d9 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 -// import ProjectListItem 1.0 +//import ProjectListItem 1.0 ApplicationWindow { visible: true @@ -12,7 +12,8 @@ ApplicationWindow { color: "whitesmoke" GridLayout { - columns: 2 + id: mainGrid + columns: 3 rows: 1 // width: 200; height: 250 @@ -22,21 +23,22 @@ ApplicationWindow { model: projectsModel delegate: Item { id: projectListItem + //property ProjectListItem listItem: projectsModel.getProject(index) width: ListView.view.width height: 40 - // property ProjectListItem listItem: projectsModel.getProject(index) - Column { - // Text { text: listItem.name } - Text { text: 'Name: ' + display.name } - Text { text: 'State: ' + display.state } - } - MouseArea { - anchors.fill: parent - onClicked: { - projectListItem.ListView.view.currentIndex = index; - view2.currentIndex = index - } - } + model: display + // Column { + // //Text { text: 'Name: ' + listItem.name } + // Text { text: 'Name: ' + name } + // Text { text: 'State: ' + state } + // } + // MouseArea { + // anchors.fill: parent + // onClicked: { + // projectListItem.ListView.view.currentIndex = index; + // view2.currentIndex = index + // } + // } } highlight: Rectangle { color: "lightsteelblue"; radius: 5 } // focus: true @@ -49,14 +51,16 @@ ApplicationWindow { Repeater { model: projectsModel Column { + id: 'col' Button { text: 'Click me' onClicked: { - // console.log('here') projectsModel.run(index, 'clean') + log.append('text text text') } } ScrollView { + id: 'sv' height: 100 TextArea { id: log @@ -71,6 +75,21 @@ ApplicationWindow { } } } + + Button { + text: 'Global' + onClicked: { + view2.children[1].col.sv.log.append('text text text') + projectsModel[1].append('text text text') + } + } } + // Connections { + // target: projectsModel + // onLogAdded { + // log.append(message) + // } + // } + } diff --git a/stm32pio/app.py b/stm32pio/app.py index a777fe5..e79cf07 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -85,7 +85,7 @@ def main(sys_argv=None) -> int: logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance handler = logging.StreamHandler() logger.addHandler(handler) - special_formatters = { 'subprocess': logging.Formatter('%(message)s') } + special_formatters = {'subprocess': logging.Formatter('%(message)s')} # Currently only 2 levels of verbosity through the '-v' option are counted (INFO (default) and DEBUG (-v)) if args is not None and args.subcommand is not None and args.verbose: logger.setLevel(logging.DEBUG) @@ -96,7 +96,7 @@ def main(sys_argv=None) -> int: elif args is not None and args.subcommand is not None: logger.setLevel(logging.INFO) handler.setFormatter(stm32pio.util.DispatchingFormatter("%(levelname)-8s %(message)s", - special=special_formatters)) + special=special_formatters)) else: logger.setLevel(logging.INFO) handler.setFormatter(logging.Formatter("%(message)s")) From d30a0cfbd71925d0b63379619645cde19a94a20b Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 31 Dec 2019 15:41:25 +0300 Subject: [PATCH 15/54] try to pass logs to QML --- stm32pio-gui/app.py | 27 +++++++++++++++++---------- stm32pio-gui/main.qml | 35 ++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index de74ce8..2378795 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -22,16 +22,15 @@ class ProjectListItem(stm32pio.lib.Stm32pio, QObject): # nameChanged = Signal() - logAdded = Signal(str, arguments=['message']) def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, qt_parent=None): QObject.__init__(self, parent=qt_parent) - # this = self + this = self class InternalHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: print(record) - # this.log(self.format(record)) + this.log(self.format(record)) self.handler = InternalHandler() logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") logger.addHandler(self.handler) @@ -43,8 +42,9 @@ def emit(self, record: logging.LogRecord) -> None: stm32pio.lib.Stm32pio.__init__(self, dirty_path, parameters=parameters, save_on_destruction=save_on_destruction) self._name = self.path.name - # def log(self, message): - # self.logAdded.emit(message) + logAdded = Signal(str, arguments=['message']) + def log(self, message): + self.logAdded.emit(message) @Property(str) def name(self): @@ -61,16 +61,21 @@ def name(self): def state(self): return str(super().state) + @Slot() + def clean(self) -> None: + print('clean was called') + super().clean() + class ProjectsList(QAbstractListModel): def __init__(self, projects: list, parent=None): super().__init__(parent) self.projects = projects - # @Slot(int) - # def getProject(self, index): - # print(index) - # return self.projects[index] + @Slot(int, result=ProjectListItem) + def getProject(self, index): + print('getProject', index) + return self.projects[index] def rowCount(self, parent=None, *args, **kwargs): return len(self.projects) @@ -108,7 +113,7 @@ def run(self, index, action): engine = QQmlApplicationEngine() - # print('AAAAAAAAAAA', qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem')) + print('AAAAAAAAAAA', qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem')) projects = ProjectsList([]) projects.addProject(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) @@ -131,5 +136,7 @@ def run(self, index, action): # view.rootContext().setContextProperty('projectsModel', projects) # view.setSource(QUrl('main.qml')) + engine.rootObjects()[0].findChildren(QObject) + engine.quit.connect(app.quit) sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index e8100d9..105d804 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 -//import ProjectListItem 1.0 +import ProjectListItem 1.0 ApplicationWindow { visible: true @@ -23,22 +23,20 @@ ApplicationWindow { model: projectsModel delegate: Item { id: projectListItem - //property ProjectListItem listItem: projectsModel.getProject(index) width: ListView.view.width height: 40 - model: display - // Column { - // //Text { text: 'Name: ' + listItem.name } - // Text { text: 'Name: ' + name } - // Text { text: 'State: ' + state } - // } - // MouseArea { - // anchors.fill: parent - // onClicked: { - // projectListItem.ListView.view.currentIndex = index; - // view2.currentIndex = index - // } - // } + Column { + //Text { text: 'Name: ' + listItem.name } + Text { text: 'Name: ' + display.name } + Text { text: 'State: ' + display.state } + } + MouseArea { + anchors.fill: parent + onClicked: { + projectListItem.ListView.view.currentIndex = index; + view2.currentIndex = index + } + } } highlight: Rectangle { color: "lightsteelblue"; radius: 5 } // focus: true @@ -52,11 +50,13 @@ ApplicationWindow { model: projectsModel Column { id: 'col' + property ProjectListItem listItem: projectsModel.getProject(index) Button { text: 'Click me' onClicked: { - projectsModel.run(index, 'clean') - log.append('text text text') + listItem.clean() + //projectsModel.run(index, 'clean') + //log.append('text text text') } } ScrollView { @@ -64,6 +64,7 @@ ApplicationWindow { height: 100 TextArea { id: log + objectName: "log" //anchors.centerIn: parent text: 'Initial log content' } From 86e0376ce21a27851f604bbabb3b3478b5555b52 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 8 Jan 2020 21:48:33 +0300 Subject: [PATCH 16/54] under heavy development... --- TODO.md | 1 + scratch.py | 47 ++-------- scratch2.py | 141 ------------------------------ stm32pio-gui/app.py | 196 +++++++++++++++++++++++++++++------------- stm32pio-gui/main.qml | 93 ++++++++++++++------ stm32pio/lib.py | 1 + 6 files changed, 212 insertions(+), 267 deletions(-) delete mode 100644 scratch2.py diff --git a/TODO.md b/TODO.md index 9b1168f..692409b 100644 --- a/TODO.md +++ b/TODO.md @@ -17,3 +17,4 @@ - [ ] logging process coverage in README - [ ] merge subprocess pipes to one where suitable - [ ] redirect subprocess pipes to DEVNULL where suitable to suppress output + - [ ] maybe move _load_config_file() to Config (i.e. config.load()) diff --git a/scratch.py b/scratch.py index 3bb14ef..a90b37e 100644 --- a/scratch.py +++ b/scratch.py @@ -1,42 +1,5 @@ -import logging - -import stm32pio.lib -import stm32pio.settings - - -class ProjectLogHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - print(record.args) - print('NEW MSG', self.format(record)) - -# root_logger = logging.getLogger('stm32pio') -# root_logger.setLevel(logging.DEBUG) - -# project = stm32pio.lib.Stm32pio('C:/Users/chufyrev/Documents/GitHub/stm32pio/stm32pio-test-project', -# parameters={'board': 'nucleo_f031k6'}, -# save_on_destruction=False) - -# string_handler = ProjectLogHandler() -# project.logger.addHandler(string_handler) -# project.logger.setLevel(logging.DEBUG) -# string_handler.setFormatter(logging.Formatter("%(levelname)-8s " -# f"%(funcName)-{stm32pio.settings.log_fieldwidth_function}s " -# "%(message)s")) - -# project.config.save() -# print(f'the state is: {str(project.state)}') -# project.generate_code() -# print(f'the state is: {str(project.state)}') - - -class Base: - def __init__(self): - print('base: ', id(self)) - -class Child(Base): - def __init__(self): - print('child 1:', id(self)) - super().__init__() - print('child 2:', id(self)) - -inst = Child() +# Second approach +view = QQuickView() +view.setResizeMode(QQuickView.SizeRootObjectToView) +view.rootContext().setContextProperty('projectsModel', projects) +view.setSource(QUrl('main.qml')) diff --git a/scratch2.py b/scratch2.py deleted file mode 100644 index 098d4b6..0000000 --- a/scratch2.py +++ /dev/null @@ -1,141 +0,0 @@ -import logging -import threading -import os -import subprocess - -# logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) - -class LogPipe(threading.Thread): - - def __init__(self, logger: logging.Logger, level: int, *args, **kwargs): - """Setup the object with a logger and a loglevel - and start the thread - """ - super().__init__(*args, **kwargs) - - self.logger = logger - self.level = level - - self.fd_read, self.fd_write = os.pipe() - self.pipe_reader = os.fdopen(self.fd_read) - - def __enter__(self): - self.start() - return self.fd_write - - def run(self): - """Run the thread, logging everything. - """ - for line in iter(self.pipe_reader.readline, ''): - self.logger.log(self.level, line.strip('\n'), 'from_subprocess') - - self.pipe_reader.close() - - def __exit__(self, exc_type, exc_val, exc_tb): - """ - The exception will be passed forward, if present so we don't need to do something with that. The tear-down - process will be done anyway - """ - os.close(self.fd_write) - - -# For testing -# if __name__ == "__main__": -# import sys -# -# logpipe = LogPipe() -# with subprocess.Popen(['curl', 'www.google.com'], stdout=logpipe, stderr=logpipe) as s: -# logpipe.close() -# -# sys.exit() - - - -old_factory = logging.getLogRecordFactory() -def record_factory(*log_record_args, **kwargs): - args_idx = 5 - if 'from_subprocess' in log_record_args[args_idx]: - new_log_record_args = log_record_args[:args_idx] + (tuple(arg for arg in log_record_args[args_idx] if arg != 'from_subprocess'),) + log_record_args[args_idx+1:] - record = old_factory(*new_log_record_args, **kwargs) - record.from_subprocess = True - else: - record = old_factory(*log_record_args, **kwargs) - return record -logging.setLogRecordFactory(record_factory) - - -class DispatchingFormatter(logging.Formatter): - def __init__(self, *args, custom_formatters=None, **kwargs): - self._formatters = custom_formatters - super().__init__(*args, **kwargs) - - def format(self, record): - if hasattr(record, 'from_subprocess') and record.from_subprocess: - try: - return self._formatters['subprocess'].format(record) - except AttributeError: - pass - return super().format(record) - - -root_l = logging.getLogger('root') -root_f = DispatchingFormatter("%(levelname)-8s %(funcName)-26s %(message)s", - custom_formatters={ - 'subprocess': logging.Formatter('ONLY MESSAGE %(message)s') - } -) -# root_f = logging.Formatter("USER HASN'T USED DispatchingFormatter %(message)s") -root_h = logging.StreamHandler() -root_h.setFormatter(root_f) -root_l.addHandler(root_h) -root_l.setLevel(logging.INFO) -# root_h.setLevel(logging.DEBUG) - - -child_l = logging.getLogger('root.child') -# child_f = DispatchingFormatter("%(levelname)-8s %(funcName)-26s %(message)s", -# custom_formatters={ -# 'subprocess': logging.Formatter('ONLY MESSAGE %(message)s') -# } -# ) -# root_f = logging.Formatter("USER HASN'T USED DispatchingFormatter %(message)s") -# child_h = logging.StreamHandler() -# child_h.setFormatter(child_f) -# child_l.addHandler(child_h) -# child_l.setLevel(logging.DEBUG) -# child_h.setLevel(logging.DEBUG) - -instance_l = logging.getLogger('root.child.instance') - - -root_l.info('Hello from root') -child_l.info('Hello from child') -instance_l.info('Hello from instance') -instance_l.info('Hello from instance but from subprocess', 'from_subprocess') - - -# logpipe = LogPipe(child_l) -# subprocess.run(['curl', 'www.google.com'], stdout=logpipe, stderr=logpipe) -# logpipe.close() - -try: - with LogPipe(instance_l, logging.DEBUG) as logpipe: - subprocess.run(['curl', 'www.google.com'], stdout=logpipe, stderr=logpipe) - # subprocess.run(['blabla'], stdout=logpipe, stderr=logpipe) - print('not called cause the exception') -except FileNotFoundError: - print('exception here') - -# with subprocess.Popen(['curl', 'www.google.com'], stdout=logpipe, stderr=logpipe) as s: -# logpipe.close() - -# popen = subprocess.Popen( -# ['curl', 'www.google.com'], -# stdout=subprocess.PIPE, -# stderr=subprocess.STDOUT, -# universal_newlines=True -# ) -# for stdout_line in iter(popen.stdout.readline, ""): -# instance_l.debug(stdout_line.strip('\n'), 'from_subprocess') -# popen.stdout.close() -# return_code = popen.wait() diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 2378795..da3a774 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -1,12 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from __future__ import annotations + +import collections +import functools import logging +import pathlib +import queue import sys +import threading import time +import weakref from PySide2.QtCore import QCoreApplication, QUrl, QAbstractItemModel, Property, QAbstractListModel, QModelIndex, \ - QObject, Qt, Slot, Signal, QTimer + QObject, Qt, Slot, Signal, QTimer, QThread from PySide2.QtGui import QGuiApplication from PySide2.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine from PySide2.QtQuick import QQuickView @@ -20,31 +28,97 @@ special_formatters = {'subprocess': logging.Formatter('%(message)s')} +class RepetitiveTimer(threading.Thread): + def __init__(self, stopped, callable, *args, **kwargs): + super().__init__(*args, **kwargs) + self.stopped = stopped + self.callable = callable + + def run(self) -> None: + print('start') + while not self.stopped.wait(timeout=0.05): + self.callable() + print('exitttt') + + +class InternalHandler(logging.Handler): + def __init__(self, parent: QObject): + super().__init__() + self.parent = parent + # self.temp_logs = [] + + self.queued_buffer = collections.deque() + + self.stopped = threading.Event() + self.timer = RepetitiveTimer(self.stopped, self.log) + self.timer.start() + + self._finalizer = weakref.finalize(self, self.at_exit) + + def at_exit(self): + print('exit') + self.stopped.set() + + def log(self): + if self.parent.is_bound: + try: + m = self.format(self.queued_buffer.popleft()) + # print('initialized', m) + self.parent.logAdded.emit(m) + except IndexError: + pass + + def emit(self, record: logging.LogRecord) -> None: + # msg = self.format(record) + # print(msg) + self.queued_buffer.append(record) + # if not self.parent.initialized: + # self.temp_logs.append(msg) + # else: + # if len(self.temp_logs): + # self.temp_logs.reverse() + # for i in range(len(self.temp_logs)): + # m = self.temp_logs.pop() + # self.parent.logAdded.emit(m) + # self.parent.logAdded.emit(msg) + + class ProjectListItem(stm32pio.lib.Stm32pio, QObject): - # nameChanged = Signal() + stateChanged = Signal() + logAdded = Signal(str, arguments=['message']) - def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, qt_parent=None): - QObject.__init__(self, parent=qt_parent) + def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, parent=None): + self.is_bound = False - this = self - class InternalHandler(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - print(record) - this.log(self.format(record)) - self.handler = InternalHandler() - logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") - logger.addHandler(self.handler) - logger.setLevel(logging.DEBUG) + QObject.__init__(self, parent=parent) + + self.handler = InternalHandler(self) + self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") + self.logger.addHandler(self.handler) + self.logger.setLevel(logging.DEBUG) self.handler.setFormatter(stm32pio.util.DispatchingFormatter( f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", special=special_formatters)) stm32pio.lib.Stm32pio.__init__(self, dirty_path, parameters=parameters, save_on_destruction=save_on_destruction) + self._name = self.path.name - logAdded = Signal(str, arguments=['message']) - def log(self, message): - self.logAdded.emit(message) + # self.destroyed.connect(self.at_exit) + self._finalizer = weakref.finalize(self, self.at_exit) + + # def update_value(): + # # m = 'SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND ' + # # print(m, flush=True) + # self.config.save() + # self.stateChanged.emit() + # # self.logAdded.emit(m) + # self.timer = threading.Timer(5, update_value) + # self.timer.start() + + def at_exit(self): + print('destroy', self) + self.logger.removeHandler(self.handler) @Property(str) def name(self): @@ -57,86 +131,88 @@ def name(self): # self._name = value # self.nameChanged.emit() - @Property(str) + @Property(str, notify=stateChanged) def state(self): return str(super().state) @Slot() - def clean(self) -> None: - print('clean was called') - super().clean() + def completed(self): + self.is_bound = True + + @Slot(str, 'QVariantList') + def run(self, action, args): + # print(action, args) + # return + this = self + def job(): + getattr(this, action)(*args) + this.stateChanged.emit() + t = threading.Thread(target=job) + t.start() + + # this = super() + # class Worker(QThread): + # def run(self): + # this.generate_code() + # self.w = Worker() + # self.w.start() + + # return super().generate_code() class ProjectsList(QAbstractListModel): + def __init__(self, projects: list, parent=None): super().__init__(parent) self.projects = projects + # self.destroyed.connect(functools.partial(ProjectsList.at_exit, self.__dict__)) + self._finalizer = weakref.finalize(self, self.at_exit) @Slot(int, result=ProjectListItem) def getProject(self, index): - print('getProject', index) return self.projects[index] def rowCount(self, parent=None, *args, **kwargs): return len(self.projects) def data(self, index: QModelIndex, role=None): - # print(index.row(), role) if role == Qt.DisplayRole: return self.projects[index.row()] - def addProject(self, project): + def add(self, project): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.projects.append(project) self.endInsertRows() - @Slot(int, str) - def run(self, index, action): - print('index:', index, action) - # time.sleep(10) - getattr(self.projects[index], action)() + @Slot(QUrl) + def addProject(self, path): + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.projects.append(ProjectListItem(path.toLocalFile(), save_on_destruction=False)) + self.endInsertRows() + # @staticmethod + def at_exit(self): + print('destroy', self) + # self.logger.removeHandler(self.handler) -if __name__ == '__main__': - # logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance - # handler = InternalHandler() - # logger.addHandler(handler) - # special_formatters = {'subprocess': logging.Formatter('%(message)s')} - # logger.setLevel(logging.DEBUG) - # handler.setFormatter(stm32pio.util.DispatchingFormatter( - # f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", - # special=special_formatters)) - # logger.debug("debug logging enabled") +if __name__ == '__main__': app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() - print('AAAAAAAAAAA', qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem')) - - projects = ProjectsList([]) - projects.addProject(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) - projects.addProject(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) - projects.addProject(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) + qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') - # def update_value(): - # projects.projects[1].name = 'def' - # timer = QTimer() - # timer.timeout.connect(update_value) - # timer.start(2000) + projects = ProjectsList([ + ProjectListItem('../stm32pio-test-project', save_on_destruction=False), + # ProjectListItem('../stm32pio-test-project', save_on_destruction=False), + # ProjectListItem('../stm32pio-test-project', save_on_destruction=False) + ]) + # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) - # First approach engine.rootContext().setContextProperty("projectsModel", projects) - engine.load(QUrl('main.qml')) - - # Second approach - # view = QQuickView() - # view.setResizeMode(QQuickView.SizeRootObjectToView) - # view.rootContext().setContextProperty('projectsModel', projects) - # view.setSource(QUrl('main.qml')) - - engine.rootObjects()[0].findChildren(QObject) - + engine.load(QUrl.fromLocalFile('main.qml')) engine.quit.connect(app.quit) + sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 105d804..fcea814 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -1,6 +1,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +import Qt.labs.platform 1.1 import ProjectListItem 1.0 @@ -13,8 +14,8 @@ ApplicationWindow { GridLayout { id: mainGrid - columns: 3 - rows: 1 + columns: 2 + rows: 2 // width: 200; height: 250 ListView { @@ -26,7 +27,6 @@ ApplicationWindow { width: ListView.view.width height: 40 Column { - //Text { text: 'Name: ' + listItem.name } Text { text: 'Name: ' + display.name } Text { text: 'State: ' + display.state } } @@ -48,49 +48,94 @@ ApplicationWindow { // anchors.fill: parent Repeater { model: projectsModel + //Component.onDestruction: console.log('DESTRUCT ALL') Column { - id: 'col' property ProjectListItem listItem: projectsModel.getProject(index) - Button { - text: 'Click me' - onClicked: { - listItem.clean() - //projectsModel.run(index, 'clean') - //log.append('text text text') + Connections { + target: listItem + onLogAdded: { + log.append(message); + } + } + Column { + ButtonGroup { + buttons: row.children + onClicked: { + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).name === button.text) { + const b = buttonsModel.get(i); + let args = []; + if (b.args) { + args = b.args.split(' '); + } + listItem.run(b.action, args); + break; + } + } + } + } + Row { + id: row + Repeater { + model: ListModel { + id: buttonsModel + ListElement { + name: 'Generate' + action: 'generate_code' + } + ListElement { + name: 'Initialize PlatformIO' + action: 'pio_init' + } + } + delegate: Button { + text: name + //rotation: -90 + } + } } } ScrollView { - id: 'sv' - height: 100 TextArea { + Component.onCompleted: listItem.completed() id: log - objectName: "log" - //anchors.centerIn: parent - text: 'Initial log content' } } Text { //anchors.centerIn: parent text: 'Name: ' + display.name } + Button { + text: 'editor' + onClicked: { + for (var i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).action === 'pio_init') { + buttonsModel.get(i).args = 'code'; + break; + } + } + } + } } } } + FolderDialog { + id: folderDialog + currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] + onAccepted: { + projectsModel.addProject(folder); + } + } + Button { - text: 'Global' + text: 'Add' onClicked: { - view2.children[1].col.sv.log.append('text text text') - projectsModel[1].append('text text text') + folderDialog.open() } } } - // Connections { - // target: projectsModel - // onLogAdded { - // log.append(message) - // } - // } + onClosing: Qt.quit() } diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 8c29f00..bb68ff7 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -129,6 +129,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction cubemx_ioc_full_filename=self.config.get('project', 'ioc_file')) self.config.set('project', 'cubemx_script_content', cubemx_script_content) + # Given parameter takes precedence over the saved one board = '' if 'board' in parameters and parameters['board'] is not None: try: From 889ce867e0338c032eb894598d1271a9c16ddc5c Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 10 Jan 2020 19:40:40 +0300 Subject: [PATCH 17/54] test on Win, UI arranging --- stm32pio-gui/app.py | 6 +-- stm32pio-gui/main.qml | 107 +++++++++++++++++++++--------------------- 2 files changed, 56 insertions(+), 57 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index da3a774..cd8134f 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -36,7 +36,7 @@ def __init__(self, stopped, callable, *args, **kwargs): def run(self) -> None: print('start') - while not self.stopped.wait(timeout=0.05): + while not self.stopped.wait(timeout=0.005): self.callable() print('exitttt') @@ -72,7 +72,7 @@ def emit(self, record: logging.LogRecord) -> None: # msg = self.format(record) # print(msg) self.queued_buffer.append(record) - # if not self.parent.initialized: + # if not self.parent.is_bound: # self.temp_logs.append(msg) # else: # if len(self.temp_logs): @@ -213,6 +213,6 @@ def at_exit(self): engine.rootContext().setContextProperty("projectsModel", projects) engine.load(QUrl.fromLocalFile('main.qml')) - engine.quit.connect(app.quit) + # engine.quit.connect(app.quit) sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index fcea814..83a4ea1 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -7,7 +7,7 @@ import ProjectListItem 1.0 ApplicationWindow { visible: true - width: 640 + width: 740 height: 480 title: qsTr("PyQt5 love QML") color: "whitesmoke" @@ -16,11 +16,9 @@ ApplicationWindow { id: mainGrid columns: 2 rows: 2 - // width: 200; height: 250 ListView { width: 200; height: 250 - // anchors.fill: parent model: projectsModel delegate: Item { id: projectListItem @@ -44,12 +42,9 @@ ApplicationWindow { SwipeView { id: view2 - width: 200; height: 250 - // anchors.fill: parent Repeater { model: projectsModel - //Component.onDestruction: console.log('DESTRUCT ALL') - Column { + delegate: Column { property ProjectListItem listItem: projectsModel.getProject(index) Connections { target: listItem @@ -57,65 +52,69 @@ ApplicationWindow { log.append(message); } } - Column { - ButtonGroup { - buttons: row.children - onClicked: { - for (let i = 0; i < buttonsModel.count; ++i) { - if (buttonsModel.get(i).name === button.text) { - const b = buttonsModel.get(i); - let args = []; - if (b.args) { - args = b.args.split(' '); - } - listItem.run(b.action, args); - break; + ButtonGroup { + buttons: row.children + onClicked: { + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).name === button.text) { + const b = buttonsModel.get(i); + let args = []; + if (b.args) { + args = b.args.split(' '); } + listItem.run(b.action, args); + break; } } } - Row { - id: row - Repeater { - model: ListModel { - id: buttonsModel - ListElement { - name: 'Generate' - action: 'generate_code' - } - ListElement { - name: 'Initialize PlatformIO' - action: 'pio_init' - } + } + Row { + id: row + Repeater { + model: ListModel { + id: buttonsModel + ListElement { + name: 'Generate' + action: 'generate_code' } - delegate: Button { - text: name - //rotation: -90 + ListElement { + name: 'Initialize PlatformIO' + action: 'pio_init' } } + delegate: Button { + text: name + //rotation: -90 + } } } - ScrollView { - TextArea { - Component.onCompleted: listItem.completed() - id: log - } - } - Text { - //anchors.centerIn: parent - text: 'Name: ' + display.name - } - Button { - text: 'editor' - onClicked: { - for (var i = 0; i < buttonsModel.count; ++i) { - if (buttonsModel.get(i).action === 'pio_init') { - buttonsModel.get(i).args = 'code'; - break; - } + Rectangle { + width: 500 + height: 380 + ScrollView { + anchors.fill: parent + TextArea { + width: 500 + height: 380 + Component.onCompleted: listItem.completed() + id: log } } } + // Text { + // text: 'Name: ' + display.name + // } + // Button { + // text: 'editor' + // onClicked: { + // for (var i = 0; i < buttonsModel.count; ++i) { + // if (buttonsModel.get(i).action === 'pio_init') { + // buttonsModel.get(i).args = 'code'; + // break; + // } + // } + // } + // } } } } From 0dca37e2f3ecc3f05107eb1f18d486912a743599 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 11 Jan 2020 21:38:35 +0300 Subject: [PATCH 18/54] modify sys.path on-the-fly --- stm32pio-gui/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index cd8134f..6840d0c 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -19,6 +19,8 @@ from PySide2.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine from PySide2.QtQuick import QQuickView +sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) + import stm32pio.settings import stm32pio.lib import stm32pio.util From 210c42c79c818eb5457b947ea5a45b453a4d62ea Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 11 Jan 2020 23:12:05 +0300 Subject: [PATCH 19/54] GUI version can now be started from a terminal --- stm32pio-gui/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 6840d0c..dd388cf 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -207,14 +207,14 @@ def at_exit(self): qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') projects = ProjectsList([ - ProjectListItem('../stm32pio-test-project', save_on_destruction=False), + ProjectListItem('stm32pio-test-project', save_on_destruction=False), # ProjectListItem('../stm32pio-test-project', save_on_destruction=False), # ProjectListItem('../stm32pio-test-project', save_on_destruction=False) ]) # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) engine.rootContext().setContextProperty("projectsModel", projects) - engine.load(QUrl.fromLocalFile('main.qml')) + engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml')) # engine.quit.connect(app.quit) sys.exit(app.exec_()) From e94f3db10bd78c56d482e90ae76abaa7041e5a48 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 12 Jan 2020 01:57:15 +0300 Subject: [PATCH 20/54] test QThread --- stm32pio-gui/app.py | 73 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index dd388cf..c542ef5 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -85,20 +85,76 @@ def emit(self, record: logging.LogRecord) -> None: # self.parent.logAdded.emit(msg) +class HandlerWorker(QObject): + addLog = Signal(str) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.temp_logs = [] + self.parent_ready = False + + this = self + class H(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + msg = self.format(record) + # print(msg) + # self.queued_buffer.append(record) + # if not this.parent_ready: + # this.temp_logs.append(msg) + # else: + # if len(this.temp_logs): + # this.temp_logs.reverse() + # for i in range(len(this.temp_logs)): + # m = this.temp_logs.pop() + # this.addLog.emit(m) + this.addLog.emit(msg) + + self.handler = H() + + # self.queued_buffer = collections.deque() + + # @Slot() + # def cccompleted(self): + # self.parent_ready = True + + # self.stopped = threading.Event() + # self.timer = RepetitiveTimer(self.stopped, self.log) + # self.timer.start() + # + # def log(self): + # if self.parent_ready: + # try: + # m = self.format(self.queued_buffer.popleft()) + # # print('initialized', m) + # self.addLog.emit(m) + # except IndexError: + # pass + + + + class ProjectListItem(stm32pio.lib.Stm32pio, QObject): stateChanged = Signal() logAdded = Signal(str, arguments=['message']) + # ccompleted = Signal() def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, parent=None): self.is_bound = False QObject.__init__(self, parent=parent) - self.handler = InternalHandler(self) + self.logThread = QThread() + self.handler = HandlerWorker() + self.handler.moveToThread(self.logThread) + self.handler.addLog.connect(self.logAdded) + # self.ccompleted.connect(self.handler.cccompleted) + self.logThread.start() + self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") - self.logger.addHandler(self.handler) + self.logger.addHandler(self.handler.handler) self.logger.setLevel(logging.DEBUG) - self.handler.setFormatter(stm32pio.util.DispatchingFormatter( + self.handler.handler.setFormatter(stm32pio.util.DispatchingFormatter( f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", special=special_formatters)) @@ -120,26 +176,21 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction def at_exit(self): print('destroy', self) + self.logThread.quit() self.logger.removeHandler(self.handler) @Property(str) def name(self): return self._name - # @name.setter - # def name(self, value): - # if self._name == value: - # return - # self._name = value - # self.nameChanged.emit() - @Property(str, notify=stateChanged) def state(self): return str(super().state) @Slot() def completed(self): - self.is_bound = True + pass + # self.handler.cccompleted() @Slot(str, 'QVariantList') def run(self, action, args): From 9db6594ccf48175131a0dd03e76921c024d053d5 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 13 Jan 2020 10:02:09 +0300 Subject: [PATCH 21/54] actions buttons --- TODO.md | 1 + stm32pio-gui/app.py | 37 +++++++++++++++++++++---------------- stm32pio-gui/main.qml | 27 +++++++++++++++++++++++---- stm32pio/lib.py | 6 ++++-- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/TODO.md b/TODO.md index 692409b..1993e77 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,7 @@ - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - [ ] 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`) + - [ ] VSCode plugin - [x] Remove casts to string where we can use path-like objects (also 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 - [ ] 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 diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index c542ef5..139e5c1 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -100,23 +100,23 @@ def emit(self, record: logging.LogRecord) -> None: msg = self.format(record) # print(msg) # self.queued_buffer.append(record) - # if not this.parent_ready: - # this.temp_logs.append(msg) - # else: - # if len(this.temp_logs): - # this.temp_logs.reverse() - # for i in range(len(this.temp_logs)): - # m = this.temp_logs.pop() - # this.addLog.emit(m) - this.addLog.emit(msg) + if not this.parent_ready: + this.temp_logs.append(msg) + else: + if len(this.temp_logs): + this.temp_logs.reverse() + for i in range(len(this.temp_logs)): + m = this.temp_logs.pop() + this.addLog.emit(m) + this.addLog.emit(msg) self.handler = H() # self.queued_buffer = collections.deque() # @Slot() - # def cccompleted(self): - # self.parent_ready = True + def cccompleted(self): + self.parent_ready = True # self.stopped = threading.Event() # self.timer = RepetitiveTimer(self.stopped, self.log) @@ -189,8 +189,8 @@ def state(self): @Slot() def completed(self): - pass - # self.handler.cccompleted() + # pass + self.handler.cccompleted() @Slot(str, 'QVariantList') def run(self, action, args): @@ -203,6 +203,9 @@ def job(): t = threading.Thread(target=job) t.start() + def save_config(self): + self.config.save() + # this = super() # class Worker(QThread): # def run(self): @@ -258,9 +261,11 @@ def at_exit(self): qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') projects = ProjectsList([ - ProjectListItem('stm32pio-test-project', save_on_destruction=False), - # ProjectListItem('../stm32pio-test-project', save_on_destruction=False), - # ProjectListItem('../stm32pio-test-project', save_on_destruction=False) + ProjectListItem('stm32pio-test-project', save_on_destruction=False, parameters={ + 'board': 'nucleo_f031k6' + }), + # ProjectListItem('stm32pio-test-project', save_on_destruction=False), + # ProjectListItem('stm32pio-test-project', save_on_destruction=False) ]) # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 83a4ea1..08861d9 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -20,6 +20,7 @@ ApplicationWindow { ListView { width: 200; height: 250 model: projectsModel + clip: true delegate: Item { id: projectListItem width: ListView.view.width @@ -42,6 +43,7 @@ ApplicationWindow { SwipeView { id: view2 + clip: true Repeater { model: projectsModel delegate: Column { @@ -51,6 +53,11 @@ ApplicationWindow { onLogAdded: { log.append(message); } + onStateChanged: { + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = false; + } + } } ButtonGroup { buttons: row.children @@ -58,10 +65,7 @@ ApplicationWindow { for (let i = 0; i < buttonsModel.count; ++i) { if (buttonsModel.get(i).name === button.text) { const b = buttonsModel.get(i); - let args = []; - if (b.args) { - args = b.args.split(' '); - } + const args = b.args ? b.args.split(' ') : []; listItem.run(b.action, args); break; } @@ -73,6 +77,10 @@ ApplicationWindow { Repeater { model: ListModel { id: buttonsModel + ListElement { + name: 'Initialize' + action: 'save_config' + } ListElement { name: 'Generate' action: 'generate_code' @@ -81,10 +89,21 @@ ApplicationWindow { name: 'Initialize PlatformIO' action: 'pio_init' } + ListElement { + name: 'Patch' + action: 'patch' + } + ListElement { + name: 'Build' + action: 'build' + } } delegate: Button { text: name //rotation: -90 + // Component.onCompleted: { + // console.log(name); + // } } } } diff --git a/stm32pio/lib.py b/stm32pio/lib.py index bb68ff7..97d417d 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -462,8 +462,7 @@ def patch(self) -> None: # Save, overwriting the original file (deletes all comments!) with self.path.joinpath('platformio.ini').open(mode='w') as platformio_ini_file: platformio_ini_config.write(platformio_ini_file) - - self.logger.info("'platformio.ini' has been patched") + self.logger.debug("'platformio.ini' has been patched") try: shutil.rmtree(self.path.joinpath('include')) @@ -473,9 +472,12 @@ def patch(self) -> None: if not self.path.joinpath('SRC').is_dir(): try: shutil.rmtree(self.path.joinpath('src')) + self.logger.debug("'src' folder has been removed") except: self.logger.info("cannot delete 'src' folder", exc_info=self.logger.isEnabledFor(logging.DEBUG)) + self.logger.info("Project has been patched") + def start_editor(self, editor_command: str) -> int: """ From d496ddd61aa19fd718ab7b8db90063b09ecd6cf0 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 16 Jan 2020 10:21:33 +0300 Subject: [PATCH 22/54] in progress --- TODO.md | 1 + scratch.py | 32 ++++++++++-- stm32pio-gui/app.py | 4 +- stm32pio-gui/main.qml | 21 ++++++-- stm32pio/lib.py | 110 +++++++++++++++++++++++++++-------------- stm32pio/tests/test.py | 14 +++--- 6 files changed, 130 insertions(+), 52 deletions(-) diff --git a/TODO.md b/TODO.md index 1993e77..05d5119 100644 --- a/TODO.md +++ b/TODO.md @@ -19,3 +19,4 @@ - [ ] merge subprocess pipes to one where suitable - [ ] redirect subprocess pipes to DEVNULL where suitable to suppress output - [ ] maybe move _load_config_file() to Config (i.e. config.load()) + - [ ] .ioc file is set in stm32pio.ini but not present in the file system anymore diff --git a/scratch.py b/scratch.py index a90b37e..0273d10 100644 --- a/scratch.py +++ b/scratch.py @@ -1,5 +1,29 @@ # Second approach -view = QQuickView() -view.setResizeMode(QQuickView.SizeRootObjectToView) -view.rootContext().setContextProperty('projectsModel', projects) -view.setSource(QUrl('main.qml')) +# view = QQuickView() +# view.setResizeMode(QQuickView.SizeRootObjectToView) +# view.rootContext().setContextProperty('projectsModel', projects) +# view.setSource(QUrl('main.qml')) + +import logging + +import stm32pio.lib +import stm32pio.settings +import stm32pio.util + +# logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance +# handler = logging.StreamHandler() +# logger.addHandler(handler) +# special_formatters = {'subprocess': logging.Formatter('%(message)s')} +# logger.setLevel(logging.DEBUG) +# handler.setFormatter(stm32pio.util.DispatchingFormatter( +# f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", +# special=special_formatters)) + +p = stm32pio.lib.Stm32pio('/Users/chufyrev/Documents/GitHub/stm32pio/stm32pio-test-project', + parameters={ 'board': 'nucleo_f031k6' }, save_on_destruction=False) +print(p.state) +print() +print([1 if v else 0 for v in p.state]) +print() +print(p.stage) +print(p.stage.value) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 139e5c1..f607f60 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -184,8 +184,8 @@ def name(self): return self._name @Property(str, notify=stateChanged) - def state(self): - return str(super().state) + def stage(self): + return str(super().stage) @Slot() def completed(self): diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 08861d9..caf9304 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -27,7 +27,7 @@ ApplicationWindow { height: 40 Column { Text { text: 'Name: ' + display.name } - Text { text: 'State: ' + display.state } + Text { text: 'State: ' + display.stage } } MouseArea { anchors.fill: parent @@ -53,11 +53,19 @@ ApplicationWindow { onLogAdded: { log.append(message); } - onStateChanged: { + Component.onCompleted: { for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = false; + // row.children[i].enabled = false; + // buttonsModel.get(i).stateChangedHandler(); + listItem.stateChanged.connect(row.children[i].haha); } } + // onStateChanged: { + // for (let i = 0; i < buttonsModel.count; ++i) { + // // row.children[i].enabled = false; + // buttonsModel.get(i).stateChangedHandler(); + // } + // } } ButtonGroup { buttons: row.children @@ -80,6 +88,9 @@ ApplicationWindow { ListElement { name: 'Initialize' action: 'save_config' + function handler() { + console.log('Inside') + } } ListElement { name: 'Generate' @@ -100,6 +111,10 @@ ApplicationWindow { } delegate: Button { text: name + signal haha + onHaha: { + model.modelData.handler(); + } //rotation: -90 // Component.onCompleted: { // console.log(name); diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 97d417d..2aa4430 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -20,13 +20,13 @@ @enum.unique -class ProjectState(enum.IntEnum): +class ProjectStage(enum.IntEnum): """ Codes indicating a project state at the moment. Should be the sequence of incrementing integers to be suited for state determining algorithm. Starting from 1 Hint: Files/folders to be present on every project state: - UNDEFINED: use this state to indicate none of the states below. Also, when we do not have any .ioc file the + EMPTY: use this state to indicate none of the states below. Also, when we do not have any .ioc file the Stm32pio class cannot be instantiated (constructor raises an exception) INITIALIZED: ['project.ioc', 'stm32pio.ini'] GENERATED: ['Inc', 'Src', 'project.ioc', 'stm32pio.ini'] @@ -37,6 +37,7 @@ class ProjectState(enum.IntEnum): .pio/build/nucleo_f031k6/firmware.elf) """ UNDEFINED = enum.auto() # note: starts from 1 + EMPTY = enum.auto() INITIALIZED = enum.auto() GENERATED = enum.auto() PIO_INITIALIZED = enum.auto() @@ -45,9 +46,10 @@ class ProjectState(enum.IntEnum): def __str__(self): string_representations = { - 'UNDEFINED': 'Undefined', - 'INITIALIZED': 'Initialized', - 'GENERATED': 'Code generated', + 'UNDEFINED': 'The project is messed up', + 'EMPTY': '.ioc file is present', + 'INITIALIZED': 'stm32pio initialized', + 'GENERATED': 'CubeMX code generated', 'PIO_INITIALIZED': 'PlatformIO project initialized', 'PATCHED': 'PlatformIO project patched', 'BUILT': 'PlatformIO project built' @@ -55,6 +57,13 @@ def __str__(self): return string_representations[self.name] +class ProjectState(collections.UserList): + def __str__(self): + return '\n'.join(('✅ ' if self[index] else '❌ ') + str(ProjectStage(index + 1)) + for index in range(1, len(self))) + + + class Config(configparser.ConfigParser): """ A simple subclass that has additional save() method for the better logic encapsulation @@ -121,12 +130,12 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction self.config = self._load_config_file() - ioc_file = self._find_ioc_file() - self.config.set('project', 'ioc_file', str(ioc_file)) + self.ioc_file = self._find_ioc_file() + self.config.set('project', 'ioc_file', str(self.ioc_file)) cubemx_script_template = string.Template(self.config.get('project', 'cubemx_script_content')) cubemx_script_content = cubemx_script_template.substitute(project_path=self.path, - cubemx_ioc_full_filename=self.config.get('project', 'ioc_file')) + cubemx_ioc_full_filename=self.ioc_file) self.config.set('project', 'cubemx_script_content', cubemx_script_content) # Given parameter takes precedence over the saved one @@ -148,18 +157,10 @@ def __repr__(self): return f"Stm32pio project: {str(self.path)}" - @property - def state(self) -> ProjectState: + def _get_states_conditions(self) -> list[int]: """ - Property returning the current state of the project. Calculated at every request - - Returns: - enum value representing a project state """ - self.logger.debug("calculating the project state...") - self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") - try: platformio_ini_is_patched = self.platformio_ini_is_patched() except: @@ -167,19 +168,19 @@ def state(self) -> ProjectState: states_conditions = collections.OrderedDict() # Fill the ordered dictionary with the conditions results - states_conditions[ProjectState.UNDEFINED] = [True] - states_conditions[ProjectState.INITIALIZED] = [ - self.path.joinpath(stm32pio.settings.config_file_name).is_file()] - states_conditions[ProjectState.GENERATED] = [self.path.joinpath('Inc').is_dir() and + states_conditions[ProjectStage.UNDEFINED] = [True] + states_conditions[ProjectStage.EMPTY] = [self.ioc_file.is_file()] + states_conditions[ProjectStage.INITIALIZED] = [self.path.joinpath(stm32pio.settings.config_file_name).is_file()] + states_conditions[ProjectStage.GENERATED] = [self.path.joinpath('Inc').is_dir() and len(list(self.path.joinpath('Inc').iterdir())) > 0, self.path.joinpath('Src').is_dir() and len(list(self.path.joinpath('Src').iterdir())) > 0] - states_conditions[ProjectState.PIO_INITIALIZED] = [ + states_conditions[ProjectStage.PIO_INITIALIZED] = [ self.path.joinpath('platformio.ini').is_file() and self.path.joinpath('platformio.ini').stat().st_size > 0] - states_conditions[ProjectState.PATCHED] = [ + states_conditions[ProjectStage.PATCHED] = [ platformio_ini_is_patched, not self.path.joinpath('include').is_dir()] - states_conditions[ProjectState.BUILT] = [ + states_conditions[ProjectStage.BUILT] = [ self.path.joinpath('.pio').is_dir() and any([item.is_file() for item in self.path.joinpath('.pio').rglob('*firmware*')])] @@ -188,34 +189,71 @@ def state(self) -> ProjectState: for state, conditions in states_conditions.items(): conditions_results.append(1 if all(condition is True for condition in conditions) else 0) + return conditions_results + + + @property + def stage(self) -> ProjectStage: + """ + Property returning the current stage of the project. Calculated at every request + + Returns: + enum value representing a project stage + """ + + self.logger.debug("calculating the project stage...") + self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") + + conditions_results = self._get_states_conditions() + # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow # propagation of this message - if self.logger.isEnabledFor(logging.DEBUG): - states_info_str = '\n'.join( - f"{state.name:20}{conditions_results[state.value - 1]}" for state in ProjectState) - self.logger.debug(f"determined states:\n{states_info_str}") + # if self.logger.isEnabledFor(logging.DEBUG): + # states_info_str = '\n'.join( + # f"{state.name:20}{conditions_results[state.value - 1]}" for state in ProjectStage) + # self.logger.debug(f"determined states:\n{states_info_str}") # Search for a consecutive sequence of 1's and find the last of them. For example, if the array is - # [1,1,0,1,0,0] - # ^ + # [1,1,1,0,1,0,0] + # ^ # we should consider 1 as the last index - last_true_index = 0 # ProjectState.UNDEFINED is always True, use as a start value + last_true_index = 0 # ProjectStage.UNDEFINED is always True, use as a start value for index, value in enumerate(conditions_results): if value == 1: last_true_index = index else: break - # Fall back to the UNDEFINED state if we have breaks in conditions results array. For example, in [1,1,0,1,0,0] + # Fall back to the UNDEFINED stage if we have breaks in conditions results array. For example, in [1,1,1,0,1,0,0] # we still return UNDEFINED as it doesn't look like a correct combination of files actually if 1 in conditions_results[last_true_index + 1:]: - project_state = ProjectState.UNDEFINED + project_state = ProjectStage.UNDEFINED else: - project_state = ProjectState(last_true_index + 1) + project_state = ProjectStage(last_true_index + 1) return project_state + @property + def state(self) -> ProjectState: + """ + """ + + self.logger.debug("calculating the project stage...") + self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") + + conditions_results = self._get_states_conditions() + + state = ProjectState() + for index, value in enumerate(conditions_results): + if value == 1: + state.append(ProjectStage(index + 1)) + else: + state.append(0) + + return state + + def _find_ioc_file(self) -> pathlib.Path: """ Find and return an .ioc file. If there are more than one, return first. If no .ioc file is present raise @@ -227,13 +265,13 @@ def _find_ioc_file(self) -> pathlib.Path: ioc_file = self.config.get('project', 'ioc_file', fallback=None) if ioc_file: - ioc_file = pathlib.Path(ioc_file).resolve() + ioc_file = pathlib.Path(ioc_file).expanduser().resolve() self.logger.debug(f"use {ioc_file.name} file from the INI config") return ioc_file else: self.logger.debug("searching for any .ioc file...") candidates = list(self.path.glob('*.ioc')) - if len(candidates) == 0: # good candidate for the new Python 3.8 assignment expressions feature :) + if len(candidates) == 0: # good candidate for the new Python 3.8 assignment expression feature :) raise FileNotFoundError("not found: CubeMX project .ioc file") elif len(candidates) == 1: self.logger.debug(f"{candidates[0].name} is selected") diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 6dbafc1..5d503ab 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -367,25 +367,25 @@ def test_get_state(self): """ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, save_on_destruction=False) - self.assertEqual(project.state, stm32pio.lib.ProjectState.UNDEFINED) + self.assertEqual(project.stage, stm32pio.lib.ProjectStage.EMPTY) project.config.save() - self.assertEqual(project.state, stm32pio.lib.ProjectState.INITIALIZED) + self.assertEqual(project.stage, stm32pio.lib.ProjectStage.INITIALIZED) project.generate_code() - self.assertEqual(project.state, stm32pio.lib.ProjectState.GENERATED) + self.assertEqual(project.stage, stm32pio.lib.ProjectStage.GENERATED) project.pio_init() - self.assertEqual(project.state, stm32pio.lib.ProjectState.PIO_INITIALIZED) + self.assertEqual(project.stage, stm32pio.lib.ProjectStage.PIO_INITIALIZED) project.patch() - self.assertEqual(project.state, stm32pio.lib.ProjectState.PATCHED) + self.assertEqual(project.stage, stm32pio.lib.ProjectStage.PATCHED) project.build() - self.assertEqual(project.state, stm32pio.lib.ProjectState.BUILT) + self.assertEqual(project.stage, stm32pio.lib.ProjectStage.BUILT) project.clean() - self.assertEqual(project.state, stm32pio.lib.ProjectState.UNDEFINED) + self.assertEqual(project.stage, stm32pio.lib.ProjectStage.EMPTY) class TestCLI(CustomTestCase): From 938ec93aa47f874a9f3b09322f46a33d9388dd9e Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 17 Jan 2020 00:44:09 +0300 Subject: [PATCH 23/54] states and stages mechanics --- scratch.py | 14 +++-- stm32pio/lib.py | 138 +++++++++++++++++++++++++----------------------- 2 files changed, 81 insertions(+), 71 deletions(-) diff --git a/scratch.py b/scratch.py index 0273d10..a0aa0a5 100644 --- a/scratch.py +++ b/scratch.py @@ -10,6 +10,16 @@ import stm32pio.settings import stm32pio.util +# s = stm32pio.lib.ProjectState([ +# (stm32pio.lib.ProjectStage.UNDEFINED, True), +# (stm32pio.lib.ProjectStage.EMPTY, True), +# (stm32pio.lib.ProjectStage.INITIALIZED, True), +# (stm32pio.lib.ProjectStage.GENERATED, True), +# (stm32pio.lib.ProjectStage.PIO_INITIALIZED, True), +# (stm32pio.lib.ProjectStage.PATCHED, False), +# (stm32pio.lib.ProjectStage.BUILT, True), +# ]) + # logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance # handler = logging.StreamHandler() # logger.addHandler(handler) @@ -23,7 +33,3 @@ parameters={ 'board': 'nucleo_f031k6' }, save_on_destruction=False) print(p.state) print() -print([1 if v else 0 for v in p.state]) -print() -print(p.stage) -print(p.stage.value) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 2aa4430..992aabc 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -57,11 +57,35 @@ def __str__(self): return string_representations[self.name] -class ProjectState(collections.UserList): +class ProjectState(collections.OrderedDict): + """ + is not protected from incorrect usage (no checks) + """ + def __str__(self): - return '\n'.join(('✅ ' if self[index] else '❌ ') + str(ProjectStage(index + 1)) - for index in range(1, len(self))) + return '\n'.join(f"{'✅ ' if state_value else '❌ '} {str(state_name)}" + for state_name, state_value in self.items() if state_name != ProjectStage.UNDEFINED) + @property + def current_stage(self): + last_consistent_state = ProjectStage.UNDEFINED + zero_found = False + + for name, value in self.items(): + if value: + if zero_found: + last_consistent_state = ProjectStage.UNDEFINED + break + else: + last_consistent_state = name + else: + zero_found = True + + return last_consistent_state + + @property + def is_consistent(self): + return self.current_stage != ProjectStage.UNDEFINED class Config(configparser.ConfigParser): @@ -157,7 +181,8 @@ def __repr__(self): return f"Stm32pio project: {str(self.path)}" - def _get_states_conditions(self) -> list[int]: + @property + def state(self): """ """ @@ -184,74 +209,53 @@ def _get_states_conditions(self) -> list[int]: self.path.joinpath('.pio').is_dir() and any([item.is_file() for item in self.path.joinpath('.pio').rglob('*firmware*')])] - # Use (1,0) instead of (True,False) because on debug printing it looks better - conditions_results = [] + conditions_results = ProjectState() for state, conditions in states_conditions.items(): - conditions_results.append(1 if all(condition is True for condition in conditions) else 0) + conditions_results[state] = all(condition is True for condition in conditions) return conditions_results - @property - def stage(self) -> ProjectStage: - """ - Property returning the current stage of the project. Calculated at every request - - Returns: - enum value representing a project stage - """ - - self.logger.debug("calculating the project stage...") - self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") - - conditions_results = self._get_states_conditions() - - # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow - # propagation of this message - # if self.logger.isEnabledFor(logging.DEBUG): - # states_info_str = '\n'.join( - # f"{state.name:20}{conditions_results[state.value - 1]}" for state in ProjectStage) - # self.logger.debug(f"determined states:\n{states_info_str}") - - # Search for a consecutive sequence of 1's and find the last of them. For example, if the array is - # [1,1,1,0,1,0,0] - # ^ - # we should consider 1 as the last index - last_true_index = 0 # ProjectStage.UNDEFINED is always True, use as a start value - for index, value in enumerate(conditions_results): - if value == 1: - last_true_index = index - else: - break - - # Fall back to the UNDEFINED stage if we have breaks in conditions results array. For example, in [1,1,1,0,1,0,0] - # we still return UNDEFINED as it doesn't look like a correct combination of files actually - if 1 in conditions_results[last_true_index + 1:]: - project_state = ProjectStage.UNDEFINED - else: - project_state = ProjectStage(last_true_index + 1) - - return project_state - - - @property - def state(self) -> ProjectState: - """ - """ - - self.logger.debug("calculating the project stage...") - self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") - - conditions_results = self._get_states_conditions() - - state = ProjectState() - for index, value in enumerate(conditions_results): - if value == 1: - state.append(ProjectStage(index + 1)) - else: - state.append(0) - - return state + # @property + # def stage(self) -> ProjectStage: + # """ + # Property returning the current stage of the project. Calculated at every request + # + # Returns: + # enum value representing a project stage + # """ + # + # self.logger.debug("calculating the project stage...") + # self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") + # + # conditions_results = self.state + # + # # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow + # # propagation of this message + # # if self.logger.isEnabledFor(logging.DEBUG): + # # states_info_str = '\n'.join( + # # f"{state.name:20}{conditions_results[state.value - 1]}" for state in ProjectStage) + # # self.logger.debug(f"determined states:\n{states_info_str}") + # + # # Search for a consecutive sequence of 1's and find the last of them. For example, if the array is + # # [1,1,1,0,1,0,0] + # # ^ + # # we should consider 1 as the last index + # last_true_index = 0 # ProjectStage.UNDEFINED is always True, use as a start value + # for index, value in enumerate(conditions_results): + # if value == 1: + # last_true_index = index + # else: + # break + # + # # Fall back to the UNDEFINED stage if we have breaks in conditions results array. For example, in [1,1,1,0,1,0,0] + # # we still return UNDEFINED as it doesn't look like a correct combination of files actually + # if 1 in conditions_results[last_true_index + 1:]: + # project_state = ProjectStage.UNDEFINED + # else: + # project_state = ProjectStage(last_true_index + 1) + # + # return project_state def _find_ioc_file(self) -> pathlib.Path: From c7bf897573fee52bcde9f75f50a2f8165aef1d31 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 17 Jan 2020 18:27:07 +0300 Subject: [PATCH 24/54] actualize tests, add 'status' CLI command --- stm32pio-gui/app.py | 10 ++++++--- stm32pio-gui/main.qml | 32 +++++++++++---------------- stm32pio/app.py | 11 +++++++--- stm32pio/lib.py | 50 +++++++----------------------------------- stm32pio/tests/test.py | 25 ++++++++------------- 5 files changed, 45 insertions(+), 83 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index f607f60..4cfb841 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -98,7 +98,7 @@ def __init__(self, *args, **kwargs): class H(logging.Handler): def emit(self, record: logging.LogRecord) -> None: msg = self.format(record) - # print(msg) + print(msg) # self.queued_buffer.append(record) if not this.parent_ready: this.temp_logs.append(msg) @@ -184,8 +184,12 @@ def name(self): return self._name @Property(str, notify=stateChanged) - def stage(self): - return str(super().stage) + def current_stage(self): + print('Hello') + return str(self.state.current_stage) + + def is_present(self): + return self.state[stm32pio.lib.ProjectStage.EMPTY] @Slot() def completed(self): diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index caf9304..fea89fc 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -9,7 +9,7 @@ ApplicationWindow { visible: true width: 740 height: 480 - title: qsTr("PyQt5 love QML") + title: "stm32pio" color: "whitesmoke" GridLayout { @@ -27,7 +27,7 @@ ApplicationWindow { height: 40 Column { Text { text: 'Name: ' + display.name } - Text { text: 'State: ' + display.stage } + Text { text: 'State: ' + display.current_stage } } MouseArea { anchors.fill: parent @@ -53,19 +53,19 @@ ApplicationWindow { onLogAdded: { log.append(message); } - Component.onCompleted: { + // Component.onCompleted: { + // for (let i = 0; i < buttonsModel.count; ++i) { + // // row.children[i].enabled = false; + // // buttonsModel.get(i).stateChangedHandler(); + // listItem.stateChanged.connect(row.children[i].haha); + // } + // } + onStateChanged: { for (let i = 0; i < buttonsModel.count; ++i) { - // row.children[i].enabled = false; + row.children[i].enabled = false; // buttonsModel.get(i).stateChangedHandler(); - listItem.stateChanged.connect(row.children[i].haha); } } - // onStateChanged: { - // for (let i = 0; i < buttonsModel.count; ++i) { - // // row.children[i].enabled = false; - // buttonsModel.get(i).stateChangedHandler(); - // } - // } } ButtonGroup { buttons: row.children @@ -88,9 +88,6 @@ ApplicationWindow { ListElement { name: 'Initialize' action: 'save_config' - function handler() { - console.log('Inside') - } } ListElement { name: 'Generate' @@ -111,11 +108,7 @@ ApplicationWindow { } delegate: Button { text: name - signal haha - onHaha: { - model.modelData.handler(); - } - //rotation: -90 + // rotation: -90 // Component.onCompleted: { // console.log(name); // } @@ -130,6 +123,7 @@ ApplicationWindow { TextArea { width: 500 height: 380 + wrapMode: Text.WordWrap Component.onCompleted: listItem.completed() id: log } diff --git a/stm32pio/app.py b/stm32pio/app.py index e79cf07..fdad551 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -38,9 +38,10 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: parser_generate = subparsers.add_parser('generate', help="generate CubeMX code only") parser_clean = subparsers.add_parser('clean', help="clean-up the project (WARNING: it deletes ALL content of " "'path' except the .ioc file)") + parser_status = subparsers.add_parser('status', help="get the description of the current project state") # Common subparsers options - for p in [parser_init, parser_new, parser_generate, parser_clean]: + for p in [parser_init, parser_new, parser_generate, parser_clean, parser_status]: p.add_argument('-d', '--directory', dest='project_path', default=pathlib.Path.cwd(), required=False, help="path to the project (current directory, if not given)") for p in [parser_init, parser_new]: @@ -75,7 +76,7 @@ def main(sys_argv=None) -> int: if sys_argv is None: sys_argv = sys.argv[1:] - # import modules after sys.path modification + # Import modules after sys.path modification import stm32pio.settings import stm32pio.lib import stm32pio.util @@ -138,7 +139,11 @@ def main(sys_argv=None) -> int: project = stm32pio.lib.Stm32pio(args.project_path, save_on_destruction=False) project.clean() - # library is designed to throw the exception in bad cases so we catch here globally + elif args.subcommand == 'status': + project = stm32pio.lib.Stm32pio(args.project_path, save_on_destruction=False) + print(project.state) + + # Library is designed to throw the exception in bad cases so we catch here globally except Exception as e: logger.exception(e, exc_info=logger.isEnabledFor(logging.DEBUG)) return -1 diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 992aabc..098fe44 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -71,9 +71,15 @@ def current_stage(self): last_consistent_state = ProjectStage.UNDEFINED zero_found = False + # Search for a consecutive sequence of 1's and find the last of them. For example, if the array is + # [1,1,1,0,1,0,0] + # ^ + # we should consider 2 as the last index for name, value in self.items(): if value: if zero_found: + # Fall back to the UNDEFINED stage if we have breaks in conditions results array. For example, in [1,1,1,0,1,0,0] + # we still return UNDEFINED as it doesn't look like a correct combination of files actually last_consistent_state = ProjectStage.UNDEFINED break else: @@ -186,6 +192,8 @@ def state(self): """ """ + # self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") + try: platformio_ini_is_patched = self.platformio_ini_is_patched() except: @@ -216,48 +224,6 @@ def state(self): return conditions_results - # @property - # def stage(self) -> ProjectStage: - # """ - # Property returning the current stage of the project. Calculated at every request - # - # Returns: - # enum value representing a project stage - # """ - # - # self.logger.debug("calculating the project stage...") - # self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") - # - # conditions_results = self.state - # - # # Put away unnecessary processing as the string still will be formed even if the logging level doesn't allow - # # propagation of this message - # # if self.logger.isEnabledFor(logging.DEBUG): - # # states_info_str = '\n'.join( - # # f"{state.name:20}{conditions_results[state.value - 1]}" for state in ProjectStage) - # # self.logger.debug(f"determined states:\n{states_info_str}") - # - # # Search for a consecutive sequence of 1's and find the last of them. For example, if the array is - # # [1,1,1,0,1,0,0] - # # ^ - # # we should consider 1 as the last index - # last_true_index = 0 # ProjectStage.UNDEFINED is always True, use as a start value - # for index, value in enumerate(conditions_results): - # if value == 1: - # last_true_index = index - # else: - # break - # - # # Fall back to the UNDEFINED stage if we have breaks in conditions results array. For example, in [1,1,1,0,1,0,0] - # # we still return UNDEFINED as it doesn't look like a correct combination of files actually - # if 1 in conditions_results[last_true_index + 1:]: - # project_state = ProjectStage.UNDEFINED - # else: - # project_state = ProjectStage(last_true_index + 1) - # - # return project_state - - def _find_ioc_file(self) -> pathlib.Path: """ Find and return an .ioc file. If there are more than one, return first. If no .ioc file is present raise diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 5d503ab..3d065c0 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -348,44 +348,37 @@ def test_regenerate_code(self): # Re-generate CubeMX project project.generate_code() - # Check if added information is preserved + # Check if added information has been preserved for test_content, after_regenerate_content in [(test_content_1, test_file_1.read_text()), (test_content_2, test_file_2.read_text())]: with self.subTest(msg=f"User content hasn't been preserved in {after_regenerate_content}"): self.assertIn(test_content, after_regenerate_content) - # main_c_after_regenerate_content = test_file_1.read_text() - # my_header_h_after_regenerate_content = test_file_2.read_text() - # self.assertIn(test_content_1, main_c_after_regenerate_content, - # msg=f"User content hasn't been preserved after regeneration in {test_file_1}") - # self.assertIn(test_content_2, my_header_h_after_regenerate_content, - # msg=f"User content hasn't been preserved after regeneration in {test_file_2}") - - def test_get_state(self): + def test_current_stage(self): """ Go through the sequence of states emulating the real-life project lifecycle """ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, save_on_destruction=False) - self.assertEqual(project.stage, stm32pio.lib.ProjectStage.EMPTY) + self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.EMPTY) project.config.save() - self.assertEqual(project.stage, stm32pio.lib.ProjectStage.INITIALIZED) + self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.INITIALIZED) project.generate_code() - self.assertEqual(project.stage, stm32pio.lib.ProjectStage.GENERATED) + self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.GENERATED) project.pio_init() - self.assertEqual(project.stage, stm32pio.lib.ProjectStage.PIO_INITIALIZED) + self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.PIO_INITIALIZED) project.patch() - self.assertEqual(project.stage, stm32pio.lib.ProjectStage.PATCHED) + self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.PATCHED) project.build() - self.assertEqual(project.stage, stm32pio.lib.ProjectStage.BUILT) + self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.BUILT) project.clean() - self.assertEqual(project.stage, stm32pio.lib.ProjectStage.EMPTY) + self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.EMPTY) class TestCLI(CustomTestCase): From fff3b007885defa8127c363755cb9dd623d85f62 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 17 Jan 2020 18:40:30 +0300 Subject: [PATCH 25/54] new TODO thoughts --- TODO.md | 13 ++++++++----- stm32pio/lib.py | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/TODO.md b/TODO.md index 05d5119..ed4b3c4 100644 --- a/TODO.md +++ b/TODO.md @@ -5,18 +5,21 @@ - [ ] Add more checks, for example when updating the project (`generate` command), check for boards matching and so on... - [ ] 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`) + - [ ] GUI. Tests - [ ] VSCode plugin - [x] Remove casts to string where we can use path-like objects (also 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 - [ ] 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?.. + - [x] `status` CLI subcommand, why not?.. - [ ] exclude tests from the bundle (see `setup.py` options) - [ ] generate code docs (help user to understand an internal kitchen, e.g. for embedding) - [ ] handle the project folder renaming/movement to other location and/or describe in README - [ ] colored logs, maybe... - [ ] check logging work when embed stm32pio lib in third-party stuff - [ ] logging process coverage in README - - [ ] merge subprocess pipes to one where suitable - - [ ] redirect subprocess pipes to DEVNULL where suitable to suppress output - - [ ] maybe move _load_config_file() to Config (i.e. config.load()) - - [ ] .ioc file is set in stm32pio.ini but not present in the file system anymore + - [ ] merge subprocess pipes to one where suitable (i.e. `stdout` and `stderr`) + - [ ] redirect subprocess pipes to `DEVNULL` where suitable to suppress output + - [ ] maybe move `_load_config_file()` to `Config` (i.e. `config.load()`) + - [ ] handle the case when the `.ioc` file is set in `stm32pio.ini` but not present in the file system anymore + - [ ] `stm32pio.ini` config file validation + - [ ] CHANGELOG markdown markup diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 098fe44..908b853 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -23,7 +23,7 @@ class ProjectStage(enum.IntEnum): """ Codes indicating a project state at the moment. Should be the sequence of incrementing integers to be suited for - state determining algorithm. Starting from 1 + state determining algorithm. Starts from 1 Hint: Files/folders to be present on every project state: EMPTY: use this state to indicate none of the states below. Also, when we do not have any .ioc file the From cc164f5b465496dde6e4a66b6249f52e27c59a7f Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 20 Jan 2020 00:53:21 +0300 Subject: [PATCH 26/54] mostly QML work --- Apple/Apple.ioc | 102 ++++++++++++++++++++++++++++++++++ Orange/Orange.ioc | 102 ++++++++++++++++++++++++++++++++++ Peach/Peach.ioc | 102 ++++++++++++++++++++++++++++++++++ stm32pio-gui/app.py | 50 +++++++++++++---- stm32pio-gui/main.qml | 121 +++++++++++++++++++++++++++++++++-------- stm32pio/lib.py | 10 +++- stm32pio/tests/test.py | 2 + 7 files changed, 450 insertions(+), 39 deletions(-) create mode 100644 Apple/Apple.ioc create mode 100644 Orange/Orange.ioc create mode 100644 Peach/Peach.ioc diff --git a/Apple/Apple.ioc b/Apple/Apple.ioc new file mode 100644 index 0000000..834d9de --- /dev/null +++ b/Apple/Apple.ioc @@ -0,0 +1,102 @@ +#MicroXplorer Configuration settings - do not modify +File.Version=6 +KeepUserPlacement=true +Mcu.Family=STM32F0 +Mcu.IP0=NVIC +Mcu.IP1=RCC +Mcu.IP2=SYS +Mcu.IP3=USART1 +Mcu.IPNb=4 +Mcu.Name=STM32F031K6Tx +Mcu.Package=LQFP32 +Mcu.Pin0=PF0-OSC_IN +Mcu.Pin1=PA2 +Mcu.Pin2=PA13 +Mcu.Pin3=PA14 +Mcu.Pin4=PA15 +Mcu.Pin5=VP_SYS_VS_Systick +Mcu.PinsNb=6 +Mcu.ThirdPartyNb=0 +Mcu.UserConstants= +Mcu.UserName=STM32F031K6Tx +MxCube.Version=5.4.0 +MxDb.Version=DB.5.0.40 +NVIC.ForceEnableDMAVector=true +NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true +PA13.GPIOParameters=GPIO_Label +PA13.GPIO_Label=SWDIO +PA13.Locked=true +PA13.Mode=Serial_Wire +PA13.Signal=SYS_SWDIO +PA14.GPIOParameters=GPIO_Label +PA14.GPIO_Label=SWCLK +PA14.Locked=true +PA14.Mode=Serial_Wire +PA14.Signal=SYS_SWCLK +PA15.GPIOParameters=GPIO_Label +PA15.GPIO_Label=VCP_RX +PA15.Locked=true +PA15.Mode=Asynchronous +PA15.Signal=USART1_RX +PA2.GPIOParameters=GPIO_Label +PA2.GPIO_Label=VCP_TX +PA2.Locked=true +PA2.Mode=Asynchronous +PA2.Signal=USART1_TX +PCC.Checker=false +PCC.Line=STM32F0x1 +PCC.MCU=STM32F031K6Tx +PCC.PartNumber=STM32F031K6Tx +PCC.Seq0=0 +PCC.Series=STM32F0 +PCC.Temperature=25 +PCC.Vdd=3.6 +PF0-OSC_IN.Locked=true +PF0-OSC_IN.Mode=HSE-External-Clock-Source +PF0-OSC_IN.Signal=RCC_OSC_IN +PinOutPanel.RotationAngle=0 +ProjectManager.AskForMigrate=true +ProjectManager.BackupPrevious=false +ProjectManager.CompilerOptimize=6 +ProjectManager.ComputerToolchain=false +ProjectManager.CoupleFile=true +ProjectManager.CustomerFirmwarePackage= +ProjectManager.DefaultFWLocation=true +ProjectManager.DeletePrevious=true +ProjectManager.DeviceId=STM32F031K6Tx +ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 +ProjectManager.FreePins=false +ProjectManager.HalAssertFull=false +ProjectManager.HeapSize=0x200 +ProjectManager.KeepUserCode=true +ProjectManager.LastFirmware=true +ProjectManager.LibraryCopy=1 +ProjectManager.MainLocation=Src +ProjectManager.NoMain=false +ProjectManager.PreviousToolchain= +ProjectManager.ProjectBuild=false +ProjectManager.ProjectFileName=stm32pio-test-project.ioc +ProjectManager.ProjectName=stm32pio-test-project +ProjectManager.StackSize=0x400 +ProjectManager.TargetToolchain=Other Toolchains (GPDSC) +ProjectManager.ToolChainLocation= +ProjectManager.UnderRoot=false +ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true +RCC.CECFreq_Value=32786.88524590164 +RCC.FamilyName=M +RCC.HSICECFreq_Value=32786.88524590164 +RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value +RCC.PLLCLKFreq_Value=8000000 +RCC.PLLMCOFreq_Value=8000000 +RCC.TimSysFreq_Value=8000000 +RCC.VCOOutput2Freq_Value=4000000 +USART1.IPParameters=VirtualMode-Asynchronous +USART1.VirtualMode-Asynchronous=VM_ASYNC +VP_SYS_VS_Systick.Mode=SysTick +VP_SYS_VS_Systick.Signal=SYS_VS_Systick +board=NUCLEO-F031K6 +boardIOC=true diff --git a/Orange/Orange.ioc b/Orange/Orange.ioc new file mode 100644 index 0000000..834d9de --- /dev/null +++ b/Orange/Orange.ioc @@ -0,0 +1,102 @@ +#MicroXplorer Configuration settings - do not modify +File.Version=6 +KeepUserPlacement=true +Mcu.Family=STM32F0 +Mcu.IP0=NVIC +Mcu.IP1=RCC +Mcu.IP2=SYS +Mcu.IP3=USART1 +Mcu.IPNb=4 +Mcu.Name=STM32F031K6Tx +Mcu.Package=LQFP32 +Mcu.Pin0=PF0-OSC_IN +Mcu.Pin1=PA2 +Mcu.Pin2=PA13 +Mcu.Pin3=PA14 +Mcu.Pin4=PA15 +Mcu.Pin5=VP_SYS_VS_Systick +Mcu.PinsNb=6 +Mcu.ThirdPartyNb=0 +Mcu.UserConstants= +Mcu.UserName=STM32F031K6Tx +MxCube.Version=5.4.0 +MxDb.Version=DB.5.0.40 +NVIC.ForceEnableDMAVector=true +NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true +PA13.GPIOParameters=GPIO_Label +PA13.GPIO_Label=SWDIO +PA13.Locked=true +PA13.Mode=Serial_Wire +PA13.Signal=SYS_SWDIO +PA14.GPIOParameters=GPIO_Label +PA14.GPIO_Label=SWCLK +PA14.Locked=true +PA14.Mode=Serial_Wire +PA14.Signal=SYS_SWCLK +PA15.GPIOParameters=GPIO_Label +PA15.GPIO_Label=VCP_RX +PA15.Locked=true +PA15.Mode=Asynchronous +PA15.Signal=USART1_RX +PA2.GPIOParameters=GPIO_Label +PA2.GPIO_Label=VCP_TX +PA2.Locked=true +PA2.Mode=Asynchronous +PA2.Signal=USART1_TX +PCC.Checker=false +PCC.Line=STM32F0x1 +PCC.MCU=STM32F031K6Tx +PCC.PartNumber=STM32F031K6Tx +PCC.Seq0=0 +PCC.Series=STM32F0 +PCC.Temperature=25 +PCC.Vdd=3.6 +PF0-OSC_IN.Locked=true +PF0-OSC_IN.Mode=HSE-External-Clock-Source +PF0-OSC_IN.Signal=RCC_OSC_IN +PinOutPanel.RotationAngle=0 +ProjectManager.AskForMigrate=true +ProjectManager.BackupPrevious=false +ProjectManager.CompilerOptimize=6 +ProjectManager.ComputerToolchain=false +ProjectManager.CoupleFile=true +ProjectManager.CustomerFirmwarePackage= +ProjectManager.DefaultFWLocation=true +ProjectManager.DeletePrevious=true +ProjectManager.DeviceId=STM32F031K6Tx +ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 +ProjectManager.FreePins=false +ProjectManager.HalAssertFull=false +ProjectManager.HeapSize=0x200 +ProjectManager.KeepUserCode=true +ProjectManager.LastFirmware=true +ProjectManager.LibraryCopy=1 +ProjectManager.MainLocation=Src +ProjectManager.NoMain=false +ProjectManager.PreviousToolchain= +ProjectManager.ProjectBuild=false +ProjectManager.ProjectFileName=stm32pio-test-project.ioc +ProjectManager.ProjectName=stm32pio-test-project +ProjectManager.StackSize=0x400 +ProjectManager.TargetToolchain=Other Toolchains (GPDSC) +ProjectManager.ToolChainLocation= +ProjectManager.UnderRoot=false +ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true +RCC.CECFreq_Value=32786.88524590164 +RCC.FamilyName=M +RCC.HSICECFreq_Value=32786.88524590164 +RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value +RCC.PLLCLKFreq_Value=8000000 +RCC.PLLMCOFreq_Value=8000000 +RCC.TimSysFreq_Value=8000000 +RCC.VCOOutput2Freq_Value=4000000 +USART1.IPParameters=VirtualMode-Asynchronous +USART1.VirtualMode-Asynchronous=VM_ASYNC +VP_SYS_VS_Systick.Mode=SysTick +VP_SYS_VS_Systick.Signal=SYS_VS_Systick +board=NUCLEO-F031K6 +boardIOC=true diff --git a/Peach/Peach.ioc b/Peach/Peach.ioc new file mode 100644 index 0000000..834d9de --- /dev/null +++ b/Peach/Peach.ioc @@ -0,0 +1,102 @@ +#MicroXplorer Configuration settings - do not modify +File.Version=6 +KeepUserPlacement=true +Mcu.Family=STM32F0 +Mcu.IP0=NVIC +Mcu.IP1=RCC +Mcu.IP2=SYS +Mcu.IP3=USART1 +Mcu.IPNb=4 +Mcu.Name=STM32F031K6Tx +Mcu.Package=LQFP32 +Mcu.Pin0=PF0-OSC_IN +Mcu.Pin1=PA2 +Mcu.Pin2=PA13 +Mcu.Pin3=PA14 +Mcu.Pin4=PA15 +Mcu.Pin5=VP_SYS_VS_Systick +Mcu.PinsNb=6 +Mcu.ThirdPartyNb=0 +Mcu.UserConstants= +Mcu.UserName=STM32F031K6Tx +MxCube.Version=5.4.0 +MxDb.Version=DB.5.0.40 +NVIC.ForceEnableDMAVector=true +NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false +NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true +PA13.GPIOParameters=GPIO_Label +PA13.GPIO_Label=SWDIO +PA13.Locked=true +PA13.Mode=Serial_Wire +PA13.Signal=SYS_SWDIO +PA14.GPIOParameters=GPIO_Label +PA14.GPIO_Label=SWCLK +PA14.Locked=true +PA14.Mode=Serial_Wire +PA14.Signal=SYS_SWCLK +PA15.GPIOParameters=GPIO_Label +PA15.GPIO_Label=VCP_RX +PA15.Locked=true +PA15.Mode=Asynchronous +PA15.Signal=USART1_RX +PA2.GPIOParameters=GPIO_Label +PA2.GPIO_Label=VCP_TX +PA2.Locked=true +PA2.Mode=Asynchronous +PA2.Signal=USART1_TX +PCC.Checker=false +PCC.Line=STM32F0x1 +PCC.MCU=STM32F031K6Tx +PCC.PartNumber=STM32F031K6Tx +PCC.Seq0=0 +PCC.Series=STM32F0 +PCC.Temperature=25 +PCC.Vdd=3.6 +PF0-OSC_IN.Locked=true +PF0-OSC_IN.Mode=HSE-External-Clock-Source +PF0-OSC_IN.Signal=RCC_OSC_IN +PinOutPanel.RotationAngle=0 +ProjectManager.AskForMigrate=true +ProjectManager.BackupPrevious=false +ProjectManager.CompilerOptimize=6 +ProjectManager.ComputerToolchain=false +ProjectManager.CoupleFile=true +ProjectManager.CustomerFirmwarePackage= +ProjectManager.DefaultFWLocation=true +ProjectManager.DeletePrevious=true +ProjectManager.DeviceId=STM32F031K6Tx +ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 +ProjectManager.FreePins=false +ProjectManager.HalAssertFull=false +ProjectManager.HeapSize=0x200 +ProjectManager.KeepUserCode=true +ProjectManager.LastFirmware=true +ProjectManager.LibraryCopy=1 +ProjectManager.MainLocation=Src +ProjectManager.NoMain=false +ProjectManager.PreviousToolchain= +ProjectManager.ProjectBuild=false +ProjectManager.ProjectFileName=stm32pio-test-project.ioc +ProjectManager.ProjectName=stm32pio-test-project +ProjectManager.StackSize=0x400 +ProjectManager.TargetToolchain=Other Toolchains (GPDSC) +ProjectManager.ToolChainLocation= +ProjectManager.UnderRoot=false +ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true +RCC.CECFreq_Value=32786.88524590164 +RCC.FamilyName=M +RCC.HSICECFreq_Value=32786.88524590164 +RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value +RCC.PLLCLKFreq_Value=8000000 +RCC.PLLMCOFreq_Value=8000000 +RCC.TimSysFreq_Value=8000000 +RCC.VCOOutput2Freq_Value=4000000 +USART1.IPParameters=VirtualMode-Asynchronous +USART1.VirtualMode-Asynchronous=VM_ASYNC +VP_SYS_VS_Systick.Mode=SysTick +VP_SYS_VS_Systick.Signal=SYS_VS_Systick +board=NUCLEO-F031K6 +boardIOC=true diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 4cfb841..1abd156 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -136,7 +136,9 @@ def cccompleted(self): class ProjectListItem(stm32pio.lib.Stm32pio, QObject): stateChanged = Signal() + stageChanged = Signal() logAdded = Signal(str, arguments=['message']) + actionResult = Signal(str, bool, arguments=['action', 'success']) # ccompleted = Signal() def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, parent=None): @@ -183,13 +185,19 @@ def at_exit(self): def name(self): return self._name - @Property(str, notify=stateChanged) + @Property('QVariant', notify=stateChanged) + def state(self): + state = super().state + return { s.name: value for s, value in state.items() } + + @Property(str, notify=stageChanged) def current_stage(self): - print('Hello') - return str(self.state.current_stage) + print('wants current_stage') + return str(super().state.current_stage) - def is_present(self): - return self.state[stm32pio.lib.ProjectStage.EMPTY] + # @Slot(result=bool) + # def is_present(self): + # return self.state[stm32pio.lib.ProjectStage.EMPTY] @Slot() def completed(self): @@ -198,17 +206,25 @@ def completed(self): @Slot(str, 'QVariantList') def run(self, action, args): - # print(action, args) - # return this = self def job(): - getattr(this, action)(*args) + try: + result = getattr(this, action)(*args) + except Exception as e: + this.logger.exception(e, exc_info=this.logger.isEnabledFor(logging.DEBUG)) + result = -1 + if result is None or (type(result) == int and result == 0): + success = True + else: + success = False this.stateChanged.emit() + this.stageChanged.emit() + this.actionResult.emit(action, success) t = threading.Thread(target=job) t.start() def save_config(self): - self.config.save() + return self.config.save() # this = super() # class Worker(QThread): @@ -250,6 +266,12 @@ def addProject(self, path): self.projects.append(ProjectListItem(path.toLocalFile(), save_on_destruction=False)) self.endInsertRows() + @Slot(int) + def removeProject(self, index): + self.beginRemoveRows(QModelIndex(), index, index) + self.projects.pop(index) + self.endRemoveRows() + # @staticmethod def at_exit(self): print('destroy', self) @@ -265,11 +287,15 @@ def at_exit(self): qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') projects = ProjectsList([ - ProjectListItem('stm32pio-test-project', save_on_destruction=False, parameters={ + ProjectListItem('Apple', save_on_destruction=False, parameters={ + 'board': 'nucleo_f031k6' + }), + ProjectListItem('Orange', save_on_destruction=False, parameters={ + 'board': 'nucleo_f031k6' + }), + ProjectListItem('Peach', save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' }), - # ProjectListItem('stm32pio-test-project', save_on_destruction=False), - # ProjectListItem('stm32pio-test-project', save_on_destruction=False) ]) # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index fea89fc..f4758b9 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -1,11 +1,14 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 -import Qt.labs.platform 1.1 +import QtQuick.Dialogs 1.3 as QtDialogs +import Qt.labs.platform 1.1 as QtLabs import ProjectListItem 1.0 + ApplicationWindow { + id: mainWindow visible: true width: 740 height: 480 @@ -18,6 +21,7 @@ ApplicationWindow { rows: 2 ListView { + id: listView width: 200; height: 250 model: projectsModel clip: true @@ -32,8 +36,8 @@ ApplicationWindow { MouseArea { anchors.fill: parent onClicked: { - projectListItem.ListView.view.currentIndex = index; - view2.currentIndex = index + listView.currentIndex = index; + swipeView.currentIndex = index; } } } @@ -42,14 +46,14 @@ ApplicationWindow { } SwipeView { - id: view2 + id: swipeView clip: true Repeater { model: projectsModel delegate: Column { property ProjectListItem listItem: projectsModel.getProject(index) Connections { - target: listItem + target: listItem // sender onLogAdded: { log.append(message); } @@ -60,15 +64,73 @@ ApplicationWindow { // listItem.stateChanged.connect(row.children[i].haha); // } // } - onStateChanged: { - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = false; - // buttonsModel.get(i).stateChangedHandler(); - } + // onStateChanged: { + // for (let i = 0; i < buttonsModel.count; ++i) { + // // row.children[i].palette.button = 'lightcoral'; + // // buttonsModel.get(i).stateChangedHandler(); + // } + // } + } + QtDialogs.MessageDialog { + id: projectIncorrectDialog + text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + + "The project will be removed from the app. It will not affect any real content" + icon: QtDialogs.StandardIcon.Critical + onAccepted: { + console.log('on accepted'); + const delIndex = swipeView.currentIndex; + listView.currentIndex = swipeView.currentIndex + 1; + swipeView.currentIndex = swipeView.currentIndex + 1; + projectsModel.removeProject(delIndex); + buttonGroup.lock = false; } } ButtonGroup { + id: buttonGroup buttons: row.children + signal stateReceived() + signal actionResult(string action, bool success) + property bool lock: false + onStateReceived: { + if (active && index == swipeView.currentIndex && !lock) { + console.log('onStateReceived', index); + + const state = projectsModel.getProject(swipeView.currentIndex).state; + listItem.stageChanged(); + + if (!state['EMPTY']) { + lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) + projectIncorrectDialog.open(); + console.log('no .ioc file'); + } else if (state['EMPTY']) { + // delete state['UNDEFINED']; + // delete state['EMPTY']; + Object.keys(state).forEach(key => { + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).state === key) { + if (state[key]) { + row.children[i].palette.button = 'lightgreen'; + } else { + row.children[i].palette.button = 'lightgray'; + } + break; + } + } + }); + } + } + } + onActionResult: { + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).action === action) { + if (success === false) { + // TODO: change to fade animation. Also, can blink a log area in the same way + row.children[i].palette.button = 'lightcoral'; + } + break; + } + } + } onClicked: { for (let i = 0; i < buttonsModel.count; ++i) { if (buttonsModel.get(i).name === button.text) { @@ -79,6 +141,13 @@ ApplicationWindow { } } } + Component.onCompleted: { + listItem.stateChanged.connect(stateReceived); + swipeView.currentItemChanged.connect(stateReceived); + mainWindow.activeChanged.connect(stateReceived); + + listItem.actionResult.connect(actionResult); + } } Row { id: row @@ -87,31 +156,33 @@ ApplicationWindow { id: buttonsModel ListElement { name: 'Initialize' + state: 'INITIALIZED' action: 'save_config' } ListElement { name: 'Generate' + state: 'GENERATED' action: 'generate_code' } ListElement { name: 'Initialize PlatformIO' + state: 'PIO_INITIALIZED' action: 'pio_init' } ListElement { name: 'Patch' + state: 'PATCHED' action: 'patch' } ListElement { name: 'Build' + state: 'BUILT' action: 'build' } } delegate: Button { text: name // rotation: -90 - // Component.onCompleted: { - // console.log(name); - // } } } } @@ -121,11 +192,14 @@ ApplicationWindow { ScrollView { anchors.fill: parent TextArea { + id: log width: 500 height: 380 + readOnly: true + selectByMouse: true wrapMode: Text.WordWrap + font.family: 'Courier' Component.onCompleted: listItem.completed() - id: log } } } @@ -135,26 +209,20 @@ ApplicationWindow { // Button { // text: 'editor' // onClicked: { - // for (var i = 0; i < buttonsModel.count; ++i) { - // if (buttonsModel.get(i).action === 'pio_init') { - // buttonsModel.get(i).args = 'code'; - // break; - // } - // } + // projectIncorrectDialog.open(); // } // } } } } - FolderDialog { + QtLabs.FolderDialog { id: folderDialog - currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] + currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] onAccepted: { projectsModel.addProject(folder); } } - Button { text: 'Add' onClicked: { @@ -163,6 +231,11 @@ ApplicationWindow { } } - onClosing: Qt.quit() + // onClosing: Qt.quit() + onActiveChanged: { + if (active) { + console.log('window received focus', swipeView.currentIndex); + } + } } diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 908b853..cd47303 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -1,5 +1,5 @@ """ -Main library +Core library """ from __future__ import annotations @@ -26,7 +26,7 @@ class ProjectStage(enum.IntEnum): state determining algorithm. Starts from 1 Hint: Files/folders to be present on every project state: - EMPTY: use this state to indicate none of the states below. Also, when we do not have any .ioc file the + UNDEFINED: use this state to indicate none of the states below. Also, when we do not have any .ioc file the Stm32pio class cannot be instantiated (constructor raises an exception) INITIALIZED: ['project.ioc', 'stm32pio.ini'] GENERATED: ['Inc', 'Src', 'project.ioc', 'stm32pio.ini'] @@ -233,16 +233,20 @@ def _find_ioc_file(self) -> pathlib.Path: absolute path to the .ioc file """ + error_message = "not found: CubeMX project .ioc file" + ioc_file = self.config.get('project', 'ioc_file', fallback=None) if ioc_file: ioc_file = pathlib.Path(ioc_file).expanduser().resolve() self.logger.debug(f"use {ioc_file.name} file from the INI config") + if (not ioc_file.is_file()): + raise FileNotFoundError(error_message) return ioc_file else: self.logger.debug("searching for any .ioc file...") candidates = list(self.path.glob('*.ioc')) if len(candidates) == 0: # good candidate for the new Python 3.8 assignment expression feature :) - raise FileNotFoundError("not found: CubeMX project .ioc file") + raise FileNotFoundError(error_message) elif len(candidates) == 1: self.logger.debug(f"{candidates[0].name} is selected") return candidates[0] diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 3d065c0..74721c8 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -380,6 +380,8 @@ def test_current_stage(self): project.clean() self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.EMPTY) + # TODO: should be undefined when the project is messed up + class TestCLI(CustomTestCase): """ From 009d17390e436b029608e4ed82c9d86f6eda0a4b Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 27 Jan 2020 01:50:55 +0300 Subject: [PATCH 27/54] BusyIndicator, Stop button research --- TODO.md | 2 + stm32pio-gui/app.py | 122 +++++++++++++++++++++--------------------- stm32pio-gui/main.qml | 41 +++++++++----- stm32pio/lib.py | 3 +- 4 files changed, 94 insertions(+), 74 deletions(-) diff --git a/TODO.md b/TODO.md index ed4b3c4..e840aaa 100644 --- a/TODO.md +++ b/TODO.md @@ -23,3 +23,5 @@ - [ ] handle the case when the `.ioc` file is set in `stm32pio.ini` but not present in the file system anymore - [ ] `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 diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 1abd156..a98495a 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -30,59 +30,59 @@ special_formatters = {'subprocess': logging.Formatter('%(message)s')} -class RepetitiveTimer(threading.Thread): - def __init__(self, stopped, callable, *args, **kwargs): - super().__init__(*args, **kwargs) - self.stopped = stopped - self.callable = callable - - def run(self) -> None: - print('start') - while not self.stopped.wait(timeout=0.005): - self.callable() - print('exitttt') - - -class InternalHandler(logging.Handler): - def __init__(self, parent: QObject): - super().__init__() - self.parent = parent - # self.temp_logs = [] - - self.queued_buffer = collections.deque() - - self.stopped = threading.Event() - self.timer = RepetitiveTimer(self.stopped, self.log) - self.timer.start() - - self._finalizer = weakref.finalize(self, self.at_exit) - - def at_exit(self): - print('exit') - self.stopped.set() - - def log(self): - if self.parent.is_bound: - try: - m = self.format(self.queued_buffer.popleft()) - # print('initialized', m) - self.parent.logAdded.emit(m) - except IndexError: - pass - - def emit(self, record: logging.LogRecord) -> None: - # msg = self.format(record) - # print(msg) - self.queued_buffer.append(record) - # if not self.parent.is_bound: - # self.temp_logs.append(msg) - # else: - # if len(self.temp_logs): - # self.temp_logs.reverse() - # for i in range(len(self.temp_logs)): - # m = self.temp_logs.pop() - # self.parent.logAdded.emit(m) - # self.parent.logAdded.emit(msg) +# class RepetitiveTimer(threading.Thread): +# def __init__(self, stopped, callable, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self.stopped = stopped +# self.callable = callable +# +# def run(self) -> None: +# print('start') +# while not self.stopped.wait(timeout=0.005): +# self.callable() +# print('exitttt') +# +# +# class InternalHandler(logging.Handler): +# def __init__(self, parent: QObject): +# super().__init__() +# self.parent = parent +# # self.temp_logs = [] +# +# self.queued_buffer = collections.deque() +# +# self.stopped = threading.Event() +# self.timer = RepetitiveTimer(self.stopped, self.log) +# self.timer.start() +# +# self._finalizer = weakref.finalize(self, self.at_exit) +# +# def at_exit(self): +# print('exit') +# self.stopped.set() +# +# def log(self): +# if self.parent.is_bound: +# try: +# m = self.format(self.queued_buffer.popleft()) +# # print('initialized', m) +# self.parent.logAdded.emit(m) +# except IndexError: +# pass +# +# def emit(self, record: logging.LogRecord) -> None: +# # msg = self.format(record) +# # print(msg) +# self.queued_buffer.append(record) +# # if not self.parent.is_bound: +# # self.temp_logs.append(msg) +# # else: +# # if len(self.temp_logs): +# # self.temp_logs.reverse() +# # for i in range(len(self.temp_logs)): +# # m = self.temp_logs.pop() +# # self.parent.logAdded.emit(m) +# # self.parent.logAdded.emit(msg) class HandlerWorker(QObject): @@ -146,7 +146,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction QObject.__init__(self, parent=parent) - self.logThread = QThread() + self.logThread = QThread() # TODO: can be a 'daemon' type as it runs alongside the main for a long time self.handler = HandlerWorker() self.handler.moveToThread(self.logThread) self.handler.addLog.connect(self.logAdded) @@ -178,8 +178,8 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction def at_exit(self): print('destroy', self) - self.logThread.quit() self.logger.removeHandler(self.handler) + self.logThread.quit() @Property(str) def name(self): @@ -290,12 +290,12 @@ def at_exit(self): ProjectListItem('Apple', save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' }), - ProjectListItem('Orange', save_on_destruction=False, parameters={ - 'board': 'nucleo_f031k6' - }), - ProjectListItem('Peach', save_on_destruction=False, parameters={ - 'board': 'nucleo_f031k6' - }), + # ProjectListItem('Orange', save_on_destruction=False, parameters={ + # 'board': 'nucleo_f031k6' + # }), + # ProjectListItem('Peach', save_on_destruction=False, parameters={ + # 'board': 'nucleo_f031k6' + # }), ]) # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index f4758b9..ec58e8e 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -26,12 +26,24 @@ ApplicationWindow { model: projectsModel clip: true delegate: Item { - id: projectListItem + id: iii width: ListView.view.width height: 40 - Column { - Text { text: 'Name: ' + display.name } - Text { text: 'State: ' + display.current_stage } + Row { + Column { + Text { text: 'Name: ' + display.name } + Text { text: 'State: ' + display.current_stage } + } + Item { + width: iii.height + height: iii.height + BusyIndicator { + anchors.centerIn: parent + running: false + width: iii.height + height: iii.height + } + } } MouseArea { anchors.fill: parent @@ -121,23 +133,25 @@ ApplicationWindow { } } onActionResult: { + stopActionButton.visible = false; for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = true; if (buttonsModel.get(i).action === action) { if (success === false) { // TODO: change to fade animation. Also, can blink a log area in the same way row.children[i].palette.button = 'lightcoral'; } - break; } } } onClicked: { for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = false; if (buttonsModel.get(i).name === button.text) { const b = buttonsModel.get(i); const args = b.args ? b.args.split(' ') : []; + stopActionButton.visible = true; listItem.run(b.action, args); - break; } } } @@ -206,12 +220,15 @@ ApplicationWindow { // Text { // text: 'Name: ' + display.name // } - // Button { - // text: 'editor' - // onClicked: { - // projectIncorrectDialog.open(); - // } - // } + Button { + id: stopActionButton + text: 'Stop' + visible: false + palette.button: 'lightcoral' + onClicked: { + // projectIncorrectDialog.open(); + } + } } } } diff --git a/stm32pio/lib.py b/stm32pio/lib.py index cd47303..63a1830 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -347,7 +347,7 @@ def generate_code(self) -> int: try: # buffering=0 leads to the immediate flushing on writing with open(cubemx_script_file, mode='w+b', buffering=0) as cubemx_script: - # encode since mode='w+b' + # should encode, since mode='w+b' cubemx_script.write(self.config.get('project', 'cubemx_script_content').encode()) self.logger.info("starting to generate a code from the CubeMX .ioc file...") @@ -368,6 +368,7 @@ def generate_code(self) -> int: "Enable a verbose output or try to generate a code from the CubeMX itself.") raise Exception("code generation error") + def pio_init(self) -> int: """ Call PlatformIO CLI to initialize a new project. It uses parameters (path, board) collected before so the From 84d734d640e3fc4990b720573b55e72b7221bdb7 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 3 Feb 2020 10:44:22 +0300 Subject: [PATCH 28/54] Async project initialization --- TODO.md | 1 + stm32pio-gui/app.py | 232 +++++++++++++++++++++++++++++------------- stm32pio-gui/main.qml | 174 +++++++++++++++++++++---------- stm32pio/app.py | 2 +- stm32pio/lib.py | 7 +- 5 files changed, 289 insertions(+), 127 deletions(-) diff --git a/TODO.md b/TODO.md index e840aaa..a86a1fd 100644 --- a/TODO.md +++ b/TODO.md @@ -25,3 +25,4 @@ - [ ] 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 diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index a98495a..fd756d4 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -84,6 +84,27 @@ # # self.parent.logAdded.emit(m) # # self.parent.logAdded.emit(msg) +class LoggingHandler(logging.Handler): + def __init__(self, signal: Signal, parent_ready_event: threading.Event): + super().__init__() + self.temp_logs = [] + self.signal = signal + self.parent_ready_event = parent_ready_event + + def emit(self, record: logging.LogRecord) -> None: + msg = self.format(record) + print(msg) + # self.queued_buffer.append(record) + if not self.parent_ready_event.is_set(): + self.temp_logs.append(msg) + else: + if len(self.temp_logs): + self.temp_logs.reverse() + for i in range(len(self.temp_logs)): + m = self.temp_logs.pop() + self.signal.emit(m) + self.signal.emit(msg) + class HandlerWorker(QObject): addLog = Signal(str) @@ -91,32 +112,16 @@ class HandlerWorker(QObject): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.temp_logs = [] - self.parent_ready = False - - this = self - class H(logging.Handler): - def emit(self, record: logging.LogRecord) -> None: - msg = self.format(record) - print(msg) - # self.queued_buffer.append(record) - if not this.parent_ready: - this.temp_logs.append(msg) - else: - if len(this.temp_logs): - this.temp_logs.reverse() - for i in range(len(this.temp_logs)): - m = this.temp_logs.pop() - this.addLog.emit(m) - this.addLog.emit(msg) - - self.handler = H() + self.parent_ready = threading.Event() + + self.logging_handler = LoggingHandler(self.addLog, self.parent_ready) # self.queued_buffer = collections.deque() # @Slot() - def cccompleted(self): - self.parent_ready = True + # def cccompleted(self): + # print('completed from ProjectListItem') + # self.parent_ready.set() # self.stopped = threading.Event() # self.timer = RepetitiveTimer(self.stopped, self.log) @@ -132,18 +137,20 @@ def cccompleted(self): # pass +class Stm32pio(stm32pio.lib.Stm32pio): + def save_config(self): + self.config.save() -class ProjectListItem(stm32pio.lib.Stm32pio, QObject): +class ProjectListItem(QObject): + nameChanged = Signal() stateChanged = Signal() stageChanged = Signal() logAdded = Signal(str, arguments=['message']) actionResult = Signal(str, bool, arguments=['action', 'success']) # ccompleted = Signal() - def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, parent=None): - self.is_bound = False - + def __init__(self, project_args: list = None, project_kwargs: dict = None, parent: QObject = None): QObject.__init__(self, parent=parent) self.logThread = QThread() # TODO: can be a 'daemon' type as it runs alongside the main for a long time @@ -151,21 +158,35 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction self.handler.moveToThread(self.logThread) self.handler.addLog.connect(self.logAdded) # self.ccompleted.connect(self.handler.cccompleted) - self.logThread.start() self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") - self.logger.addHandler(self.handler.handler) + self.logger.addHandler(self.handler.logging_handler) self.logger.setLevel(logging.DEBUG) - self.handler.handler.setFormatter(stm32pio.util.DispatchingFormatter( + self.handler.logging_handler.setFormatter(stm32pio.util.DispatchingFormatter( f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", special=special_formatters)) - stm32pio.lib.Stm32pio.__init__(self, dirty_path, parameters=parameters, save_on_destruction=save_on_destruction) + self.logThread.start() + + self.worker = ProjectActionWorker(self.logger, lambda: None) + + self.project = None + self._name = 'Loading...' + self._state = { 'LOADING': True } + self._current_stage = 'Loading...' - self._name = self.path.name + self.qml_ready = threading.Event() # self.destroyed.connect(self.at_exit) - self._finalizer = weakref.finalize(self, self.at_exit) + self._finalizer2 = weakref.finalize(self, self.at_exit) + + if project_args is not None: + if 'logger' not in project_kwargs: + project_kwargs['logger'] = self.logger + + self.init_thread = threading.Thread(target=self.init_project, args=project_args, kwargs=project_kwargs) + self.init_thread.start() + # self.init_project(*project_args, **project_kwargs) # def update_value(): # # m = 'SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND ' @@ -176,24 +197,50 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction # self.timer = threading.Timer(5, update_value) # self.timer.start() + def init_project(self, *args, **kwargs): + try: + # import time + # time.sleep(1) + if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': + raise Exception("Error during initialization") + self.project = Stm32pio(*args, **kwargs) + except Exception as e: + self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG)) + self._name = args[0] # FIXME + self._state = { 'INIT_ERROR': True } + self._current_stage = 'Initializing error' + self.qml_ready.wait() + self.nameChanged.emit() + self.stageChanged.emit() + self.stateChanged.emit() + # self.worker = ProjectActionWorker(self.logger, job) + def at_exit(self): print('destroy', self) self.logger.removeHandler(self.handler) self.logThread.quit() - @Property(str) + @Property(str, notify=nameChanged) def name(self): - return self._name + if self.project is not None: + return self.project.path.name + else: + return self._name @Property('QVariant', notify=stateChanged) def state(self): - state = super().state - return { s.name: value for s, value in state.items() } + if self.project is not None: + return { s.name: value for s, value in self.project.state.items() } + else: + return self._state @Property(str, notify=stageChanged) def current_stage(self): - print('wants current_stage') - return str(super().state.current_stage) + # print('wants current_stage') + if self.project is not None: + return str(self.project.state.current_stage) + else: + return self._current_stage # @Slot(result=bool) # def is_present(self): @@ -201,30 +248,28 @@ def current_stage(self): @Slot() def completed(self): - # pass - self.handler.cccompleted() + print('completed from QML') + self.qml_ready.set() + self.handler.parent_ready.set() + # self.handler.cccompleted() @Slot(str, 'QVariantList') def run(self, action, args): - this = self - def job(): - try: - result = getattr(this, action)(*args) - except Exception as e: - this.logger.exception(e, exc_info=this.logger.isEnabledFor(logging.DEBUG)) - result = -1 - if result is None or (type(result) == int and result == 0): - success = True - else: - success = False - this.stateChanged.emit() - this.stageChanged.emit() - this.actionResult.emit(action, success) - t = threading.Thread(target=job) - t.start() + self.worker = ProjectActionWorker(self.logger, getattr(self.project, action), args) + + self.worker.actionResult.connect(self.stateChanged) + self.worker.actionResult.connect(self.stageChanged) + self.worker.actionResult.connect(self.actionResult) - def save_config(self): - return self.config.save() + + # @Slot(str) + # def stop(self, action): + # if self.worker.thread.isRunning() and self.worker.name == action: + # print('===============================', self.worker.thread.quit()) + + + # def save_config(self): + # return self.config.save() # this = super() # class Worker(QThread): @@ -236,12 +281,47 @@ def save_config(self): # return super().generate_code() +class ProjectActionWorker(QObject): + actionResult = Signal(str, bool, arguments=['action', 'success']) + + def __init__(self, logger, func, args=None): + super().__init__(parent=None) # QObject with a parent cannot be moved to any thread + + self.logger = logger + self.func = func + if args is None: + self.args = [] + else: + self.args = args + self.name = func.__name__ + + self.thread = QThread() + self.moveToThread(self.thread) + self.actionResult.connect(self.thread.quit) + + self.thread.started.connect(self.job) + self.thread.start() + + + def job(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.actionResult.emit(self.name, success) + + class ProjectsList(QAbstractListModel): def __init__(self, projects: list, parent=None): super().__init__(parent) self.projects = projects - # self.destroyed.connect(functools.partial(ProjectsList.at_exit, self.__dict__)) self._finalizer = weakref.finalize(self, self.at_exit) @Slot(int, result=ProjectListItem) @@ -263,8 +343,19 @@ def add(self, project): @Slot(QUrl) def addProject(self, path): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) - self.projects.append(ProjectListItem(path.toLocalFile(), save_on_destruction=False)) + project = ProjectListItem(project_args=[path.toLocalFile()], + project_kwargs=dict(save_on_destruction=False, parameters={'board': 'nucleo_f031k6'})) + # project = ProjectListItem() + self.projects.append(project) self.endInsertRows() + # project.init_project(path.toLocalFile(), save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' }, logger=project.logger) + + # self.adding_project = None + # def job(): + # self.adding_project = ProjectListItem(path.toLocalFile(), save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' }) + # self.adding_project.moveToThread(app.thread()) + # self.worker = ProjectActionWorker(None, job) + # self.worker.actionResult.connect(self.add) @Slot(int) def removeProject(self, index): @@ -272,7 +363,6 @@ def removeProject(self, index): self.projects.pop(index) self.endRemoveRows() - # @staticmethod def at_exit(self): print('destroy', self) # self.logger.removeHandler(self.handler) @@ -286,20 +376,20 @@ def at_exit(self): qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') + apple = ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) + # orange = ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) + # peach = ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) + # apple.init_project() + projects = ProjectsList([ - ProjectListItem('Apple', save_on_destruction=False, parameters={ - 'board': 'nucleo_f031k6' - }), - # ProjectListItem('Orange', save_on_destruction=False, parameters={ - # 'board': 'nucleo_f031k6' - # }), - # ProjectListItem('Peach', save_on_destruction=False, parameters={ - # 'board': 'nucleo_f031k6' - # }), + apple, + # orange, + # peach ]) + # projects.addProject('Apple') # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) - engine.rootContext().setContextProperty("projectsModel", projects) + engine.rootContext().setContextProperty('projectsModel', projects) engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml')) # engine.quit.connect(app.quit) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index ec58e8e..b4f59fa 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -10,11 +10,26 @@ import ProjectListItem 1.0 ApplicationWindow { id: mainWindow visible: true - width: 740 + width: 790 height: 480 title: "stm32pio" color: "whitesmoke" + // Popup { + // id: popup + // anchors.centerIn: parent + // modal: true + + // parent: Overlay.overlay + // background: Rectangle { + // color: '#00000000' + // } + // contentItem: Column { + // BusyIndicator {} + // Text { text: 'Loading...' } + // } + // } + GridLayout { id: mainGrid columns: 2 @@ -22,27 +37,34 @@ ApplicationWindow { ListView { id: listView - width: 200; height: 250 + width: 250; height: 250 model: projectsModel clip: true delegate: Item { id: iii + property bool loading: true + property bool actionRunning: false width: ListView.view.width height: 40 + property ProjectListItem listItem: projectsModel.getProject(index) + Connections { + target: listItem // sender + onNameChanged: { + loading = false; + } + onActionResult: { + actionRunning = false; + } + } Row { Column { Text { text: 'Name: ' + display.name } - Text { text: 'State: ' + display.current_stage } + Text { text: 'Stage: ' + display.current_stage } } - Item { + BusyIndicator { + running: iii.loading || iii.actionRunning width: iii.height height: iii.height - BusyIndicator { - anchors.centerIn: parent - running: false - width: iii.height - height: iii.height - } } } MouseArea { @@ -69,6 +91,11 @@ ApplicationWindow { onLogAdded: { log.append(message); } + onNameChanged: { + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = true; + } + } // Component.onCompleted: { // for (let i = 0; i < buttonsModel.count; ++i) { // // row.children[i].enabled = false; @@ -101,22 +128,27 @@ ApplicationWindow { id: buttonGroup buttons: row.children signal stateReceived() - signal actionResult(string action, bool success) + signal actionResult(string actionDone, bool success) property bool lock: false onStateReceived: { if (active && index == swipeView.currentIndex && !lock) { - console.log('onStateReceived', index); + console.log('onStateReceived', active, index, !lock); const state = projectsModel.getProject(swipeView.currentIndex).state; listItem.stageChanged(); - if (!state['EMPTY']) { + if (state['LOADING']) { + // listView.currentItem.running = true; + } else if (state['INIT_ERROR']) { + // listView.currentItem.running = false; + row.visible = false; + initErrorMessage.visible = true; + } else if (!state['EMPTY']) { lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) projectIncorrectDialog.open(); console.log('no .ioc file'); } else if (state['EMPTY']) { - // delete state['UNDEFINED']; - // delete state['EMPTY']; + // listView.currentItem.running = false; Object.keys(state).forEach(key => { for (let i = 0; i < buttonsModel.count; ++i) { if (buttonsModel.get(i).state === key) { @@ -132,29 +164,30 @@ ApplicationWindow { } } } - onActionResult: { - stopActionButton.visible = false; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = true; - if (buttonsModel.get(i).action === action) { - if (success === false) { - // TODO: change to fade animation. Also, can blink a log area in the same way - row.children[i].palette.button = 'lightcoral'; - } - } - } - } - onClicked: { - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = false; - if (buttonsModel.get(i).name === button.text) { - const b = buttonsModel.get(i); - const args = b.args ? b.args.split(' ') : []; - stopActionButton.visible = true; - listItem.run(b.action, args); - } - } - } + // onActionResult: { + // // stopActionButton.visible = false; + // for (let i = 0; i < buttonsModel.count; ++i) { + // row.children[i].enabled = true; + // if (buttonsModel.get(i).action === action) { + // if (success === false) { + // // TODO: change to fade animation. Also, can blink a log area in the same way + // row.children[i].palette.button = 'lightcoral'; + // } + // } + // } + // } + // onClicked: { + // // stopActionButton.visible = true; + // listView.currentItem.actionRunning = true; + // for (let i = 0; i < buttonsModel.count; ++i) { + // row.children[i].enabled = false; + // if (buttonsModel.get(i).name === button.text) { + // const b = buttonsModel.get(i); + // const args = b.args ? b.args.split(' ') : []; + // listItem.run(b.action, args); + // } + // } + // } Component.onCompleted: { listItem.stateChanged.connect(stateReceived); swipeView.currentItemChanged.connect(stateReceived); @@ -163,6 +196,13 @@ ApplicationWindow { listItem.actionResult.connect(actionResult); } } + Text { + id: initErrorMessage + visible: false + padding: 10 + text: "The project cannot be initialized" + color: 'red' + } Row { id: row Repeater { @@ -196,7 +236,24 @@ ApplicationWindow { } delegate: Button { text: name + enabled: false // rotation: -90 + onClicked: { + // enabled = false; + listView.currentItem.actionRunning = true; + const args = model.args ? model.args.split(' ') : []; + listItem.run(model.action, args); + } + Connections { + target: buttonGroup + onActionResult: { + // console.log('actionDone', actionDone, model.name); + if (actionDone === model.action && success === false) { + palette.button = 'lightcoral'; + } + // enabled = true; + } + } } } } @@ -207,6 +264,7 @@ ApplicationWindow { anchors.fill: parent TextArea { id: log + // anchors.fill: parent width: 500 height: 380 readOnly: true @@ -220,15 +278,22 @@ ApplicationWindow { // Text { // text: 'Name: ' + display.name // } - Button { - id: stopActionButton - text: 'Stop' - visible: false - palette.button: 'lightcoral' - onClicked: { - // projectIncorrectDialog.open(); - } - } + // Button { + // text: 'test' + // onClicked: { + // console.log(); + // } + // } + // Button { + // id: stopActionButton + // text: 'Stop' + // visible: false + // palette.button: 'lightcoral' + // onClicked: { + // // projectIncorrectDialog.open(); + // console.log(listItem.stop('generate_code')); + // } + // } } } } @@ -237,22 +302,25 @@ ApplicationWindow { id: folderDialog currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] onAccepted: { + // popup.open(); projectsModel.addProject(folder); + // listView.currentIndex = listView.count; + // swipeView.currentIndex = listView.count; } } Button { text: 'Add' onClicked: { - folderDialog.open() + folderDialog.open(); } } } // onClosing: Qt.quit() - onActiveChanged: { - if (active) { - console.log('window received focus', swipeView.currentIndex); - } - } + // onActiveChanged: { + // if (active) { + // console.log('window received focus', swipeView.currentIndex); + // } + // } } diff --git a/stm32pio/app.py b/stm32pio/app.py index fdad551..e803e22 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -111,7 +111,7 @@ def main(sys_argv=None) -> int: if not args.board: logger.warning("STM32 PlatformIO board is not specified, it will be needed on PlatformIO project " "creation") - logger.info('project has been initialized. You can now edit stm32pio.ini config file') + logger.info("project has been initialized. You can now edit stm32pio.ini config file") if args.editor: project.start_editor(args.editor) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 63a1830..081c096 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -145,11 +145,14 @@ class Stm32pio: save_on_destruction (bool): register or not the finalizer that saves the config to file """ - def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True): + def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, logger: logging.Logger = None): if parameters is None: parameters = {} - self.logger = logging.getLogger(f"{__name__}.{id(self)}") + if logger is not None: + self.logger = logger + else: + self.logger = logging.getLogger(f"{__name__}.{id(self)}") # The path is a unique identifier of the project so it would be great to remake Stm32pio class as a subclass of # pathlib.Path and then reference it like self and not self.path. It is more consistent also, as now path is From 2bafb2f43b30ca671a56c588cc176d59806986a9 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 4 Feb 2020 19:13:19 +0300 Subject: [PATCH 29/54] glowing animation after running a project action, Windows QML debugging output support --- stm32pio-gui/app.py | 19 ++++++++- stm32pio-gui/main.qml | 93 +++++++++++++++++++++++++++++-------------- 2 files changed, 82 insertions(+), 30 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index fd756d4..bf6aeba 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -14,7 +14,8 @@ import weakref from PySide2.QtCore import QCoreApplication, QUrl, QAbstractItemModel, Property, QAbstractListModel, QModelIndex, \ - QObject, Qt, Slot, Signal, QTimer, QThread + QObject, Qt, Slot, Signal, QTimer, QThread, qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, \ + QtFatalMsg from PySide2.QtGui import QGuiApplication from PySide2.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine from PySide2.QtQuick import QQuickView @@ -368,8 +369,24 @@ def at_exit(self): # self.logger.removeHandler(self.handler) +def qt_message_handler(mode, context, message): + if mode == QtInfoMsg: + mode = 'Info' + elif mode == QtWarningMsg: + mode = 'Warning' + elif mode == QtCriticalMsg: + mode = 'critical' + elif mode == QtFatalMsg: + mode = 'fatal' + else: + mode = 'Debug' + print("%s: %s" % (mode, message)) + if __name__ == '__main__': + if stm32pio.settings.my_os == 'Windows': + qInstallMessageHandler(qt_message_handler) + app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index b4f59fa..9174935 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -1,6 +1,7 @@ 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 @@ -10,7 +11,7 @@ import ProjectListItem 1.0 ApplicationWindow { id: mainWindow visible: true - width: 790 + width: 830 height: 480 title: "stm32pio" color: "whitesmoke" @@ -51,6 +52,7 @@ ApplicationWindow { target: listItem // sender onNameChanged: { loading = false; + // TODO: open the dialog where the user can enter board, editor etc. } onActionResult: { actionRunning = false; @@ -164,30 +166,26 @@ ApplicationWindow { } } } - // onActionResult: { - // // stopActionButton.visible = false; - // for (let i = 0; i < buttonsModel.count; ++i) { - // row.children[i].enabled = true; - // if (buttonsModel.get(i).action === action) { - // if (success === false) { - // // TODO: change to fade animation. Also, can blink a log area in the same way - // row.children[i].palette.button = 'lightcoral'; - // } - // } - // } - // } - // onClicked: { - // // stopActionButton.visible = true; - // listView.currentItem.actionRunning = true; - // for (let i = 0; i < buttonsModel.count; ++i) { - // row.children[i].enabled = false; - // if (buttonsModel.get(i).name === button.text) { - // const b = buttonsModel.get(i); - // const args = b.args ? b.args.split(' ') : []; - // listItem.run(b.action, args); - // } - // } - // } + onActionResult: { + // stopActionButton.visible = false; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = true; + } + } + onClicked: { + // stopActionButton.visible = true; + // listView.currentItem.actionRunning = true; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = false; + row.children[i].glowingVisible = false; + row.children[i].anim.complete(); + // if (buttonsModel.get(i).name === button.text) { + // const b = buttonsModel.get(i); + // const args = b.args ? b.args.split(' ') : []; + // listItem.run(b.action, args); + // } + } + } Component.onCompleted: { listItem.stateChanged.connect(stateReceived); swipeView.currentItemChanged.connect(stateReceived); @@ -205,6 +203,9 @@ ApplicationWindow { } Row { id: row + padding: 10 + spacing: 10 + z: 1 Repeater { model: ListModel { id: buttonsModel @@ -235,11 +236,13 @@ ApplicationWindow { } } delegate: Button { + id: actionButton text: name enabled: false + property alias glowingVisible: glow.visible + property alias anim: seq // rotation: -90 onClicked: { - // enabled = false; listView.currentItem.actionRunning = true; const args = model.args ? model.args.split(' ') : []; listItem.run(model.action, args); @@ -248,10 +251,41 @@ ApplicationWindow { target: buttonGroup onActionResult: { // console.log('actionDone', actionDone, model.name); - if (actionDone === model.action && success === false) { - palette.button = 'lightcoral'; + if (actionDone === model.action) { + if (success) { + glow.color = 'lightgreen'; + } else { + palette.button = 'lightcoral'; + glow.color = 'lightcoral'; + } + glow.visible = true; + seq.start(); } - // enabled = true; + } + } + Glow { + id: glow + visible: false + anchors.fill: actionButton + radius: 10 + samples: 21 + color: 'lightgreen' + source: actionButton + } + SequentialAnimation { + id: seq + loops: 3 + OpacityAnimator { + target: glow + from: 0 + to: 1 + duration: 1000 + } + OpacityAnimator { + target: glow + from: 1 + to: 0 + duration: 1000 } } } @@ -304,6 +338,7 @@ ApplicationWindow { onAccepted: { // popup.open(); projectsModel.addProject(folder); + // listView.currentIndex = listView.count; // swipeView.currentIndex = listView.count; } From f8e4124f29f6affc0cc1b6024604989d114050f3 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 5 Feb 2020 01:22:49 +0300 Subject: [PATCH 30/54] in progress --- stm32pio-gui/app.py | 19 +++++++------------ stm32pio-gui/main.qml | 29 ++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index bf6aeba..bc12845 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -202,15 +202,15 @@ def init_project(self, *args, **kwargs): try: # import time # time.sleep(1) - if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': - raise Exception("Error during initialization") + # if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': + # raise Exception("Error during initialization") self.project = Stm32pio(*args, **kwargs) except Exception as e: self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG)) - self._name = args[0] # FIXME + self._name = args[0] # FIXME check if available self._state = { 'INIT_ERROR': True } self._current_stage = 'Initializing error' - self.qml_ready.wait() + self.qml_ready.wait() # FIXME still not guaranteed, should check for ALL components to be loaded self.nameChanged.emit() self.stageChanged.emit() self.stateChanged.emit() @@ -393,15 +393,10 @@ def qt_message_handler(mode, context, message): qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') - apple = ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) - # orange = ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) - # peach = ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) - # apple.init_project() - projects = ProjectsList([ - apple, - # orange, - # peach + ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })), + # ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })), + # ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) ]) # projects.addProject('Apple') # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 9174935..26c5a6a 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -84,6 +84,8 @@ ApplicationWindow { SwipeView { id: swipeView clip: true + interactive: false + orientation: Qt.Vertical Repeater { model: projectsModel delegate: Column { @@ -209,6 +211,12 @@ ApplicationWindow { Repeater { model: ListModel { id: buttonsModel + // ListElement { + // // TODO: add margin or divider or smth to visually separate the Clean action as it doesn't represent any state + // name: 'Clean' + // // state: 'INITIALIZED' + // action: 'clean' + // } ListElement { name: 'Initialize' state: 'INITIALIZED' @@ -315,7 +323,7 @@ ApplicationWindow { // Button { // text: 'test' // onClicked: { - // console.log(); + // row.visible = false; // } // } // Button { @@ -328,6 +336,25 @@ ApplicationWindow { // console.log(listItem.stop('generate_code')); // } // } + Column { + id: initDialog + // visible: false + Text { + text: 'You can specify blabla' + } + Row { + TextField { + placeholderText: 'Board' + } + TextField { + placeholderText: 'Editor' + } + CheckBox { + text: 'Build' + enabled: false + } + } + } } } } From 2c2cbb29ba54c311b94b7e8f62bf1741110e4661 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 5 Feb 2020 22:24:59 +0300 Subject: [PATCH 31/54] Shift and Ctrl clicks, RectangularGlow --- stm32pio-gui/app.py | 17 +++--- stm32pio-gui/main.qml | 122 +++++++++++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 24 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index bc12845..08c316b 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -94,7 +94,7 @@ def __init__(self, signal: Signal, parent_ready_event: threading.Event): def emit(self, record: logging.LogRecord) -> None: msg = self.format(record) - print(msg) + # print(msg) # self.queued_buffer.append(record) if not self.parent_ready_event.is_set(): self.temp_logs.append(msg) @@ -210,11 +210,15 @@ def init_project(self, *args, **kwargs): self._name = args[0] # FIXME check if available self._state = { 'INIT_ERROR': True } self._current_stage = 'Initializing error' - self.qml_ready.wait() # FIXME still not guaranteed, should check for ALL components to be loaded - self.nameChanged.emit() - self.stageChanged.emit() - self.stateChanged.emit() - # self.worker = ProjectActionWorker(self.logger, job) + else: + # TODO: maybe remove _-values + pass + finally: + self.qml_ready.wait() # FIXME still not guaranteed, should check for ALL components to be loaded + self.nameChanged.emit() + self.stageChanged.emit() + self.stateChanged.emit() + # self.worker = ProjectActionWorker(self.logger, job) def at_exit(self): print('destroy', self) @@ -256,6 +260,7 @@ def completed(self): @Slot(str, 'QVariantList') def run(self, action, args): + # TODO: queue or smth of jobs self.worker = ProjectActionWorker(self.logger, getattr(self.project, action), args) self.worker.actionResult.connect(self.stateChanged) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 26c5a6a..9c7b277 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -136,7 +136,7 @@ ApplicationWindow { property bool lock: false onStateReceived: { if (active && index == swipeView.currentIndex && !lock) { - console.log('onStateReceived', active, index, !lock); + // console.log('onStateReceived', active, index, !lock); const state = projectsModel.getProject(swipeView.currentIndex).state; listItem.stageChanged(); @@ -203,20 +203,24 @@ ApplicationWindow { text: "The project cannot be initialized" color: 'red' } - Row { + RowLayout { id: row - padding: 10 - spacing: 10 + // padding: 10 + // spacing: 10 z: 1 Repeater { model: ListModel { id: buttonsModel - // ListElement { - // // TODO: add margin or divider or smth to visually separate the Clean action as it doesn't represent any state - // name: 'Clean' - // // state: 'INITIALIZED' - // action: 'clean' - // } + ListElement { + name: 'Clean' + action: 'clean' + } + ListElement { + name: 'Open editor' + action: 'start_editor' + args: 'code' + margin: 15 // margin to visually separate the Clean action as it doesn't represent any state + } ListElement { name: 'Initialize' state: 'INITIALIZED' @@ -244,17 +248,86 @@ ApplicationWindow { } } delegate: Button { - id: actionButton text: name enabled: false property alias glowingVisible: glow.visible property alias anim: seq + Layout.margins: 10 // insets can be used too + Layout.rightMargin: margin // rotation: -90 - onClicked: { + function runAction() { listView.currentItem.actionRunning = true; const args = model.args ? model.args.split(' ') : []; listItem.run(model.action, args); } + onClicked: { + runAction(); + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + property bool ctrlPressed: false + property bool ctrlPressedLastState: false + property bool shiftPressed: false + property bool shiftPressedLastState: false + function h() { + console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button + } + function shiftHandler() { + console.log('shiftHandler', shiftPressed, index); + for (let i = 2; i <= index; ++i) { + if (shiftPressed) { + if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { + row.children[i].palette.button = 'honeydew'; + } + } else { + if (Qt.colorEqual(row.children[i].palette.button, 'honeydew')) { + row.children[i].palette.button = 'lightgray'; + } + } + } + } + onClicked: { + parent.clicked(); // propagateComposedEvents: true // doesn't work + if (ctrlPressed && model.action !== 'start_editor') { + model.shouldStartEditor = true; + } + if (shiftPressed) { + // run all actions in series + } + } + onPositionChanged: { + if (mouse.modifiers & Qt.ControlModifier) { + ctrlPressed = true; + } else { + ctrlPressed = false; + } + if (ctrlPressedLastState !== ctrlPressed) { + ctrlPressedLastState = ctrlPressed; + h(); + } + + if (mouse.modifiers & Qt.ShiftModifier) { + shiftPressed = true; + } else { + shiftPressed = false; + } + if (shiftPressedLastState !== shiftPressed) { + shiftPressedLastState = shiftPressed; + shiftHandler(); + } + } + onExited: { + ctrlPressed = false; + ctrlPressedLastState = false; + + if (shiftPressed || shiftPressedLastState) { + shiftPressed = false; + shiftPressedLastState = false; + shiftHandler(); + } + } + } Connections { target: buttonGroup onActionResult: { @@ -268,17 +341,30 @@ ApplicationWindow { } glow.visible = true; seq.start(); + + if (model.shouldStartEditor) { + model.shouldStartEditor = false; + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).action === 'start_editor') { + row.children[i].runAction(); + break; + } + } + } } } } - Glow { + RectangularGlow { id: glow visible: false - anchors.fill: actionButton - radius: 10 - samples: 21 - color: 'lightgreen' - source: actionButton + anchors.fill: parent + cornerRadius: 25 + glowRadius: 20 + spread: 0.25 + // radius: 10 + // samples: 21 + // color: 'lightgreen' + // source: actionButton } SequentialAnimation { id: seq From 22a7ca7ffeab18fc86b07d60ce1e8b1f23349745 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 6 Feb 2020 01:11:22 +0300 Subject: [PATCH 32/54] action buttons improvements --- stm32pio-gui/main.qml | 60 ++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 9c7b277..7055024 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -11,8 +11,8 @@ import ProjectListItem 1.0 ApplicationWindow { id: mainWindow visible: true - width: 830 - height: 480 + width: 1130 + height: 550 title: "stm32pio" color: "whitesmoke" @@ -93,7 +93,7 @@ ApplicationWindow { Connections { target: listItem // sender onLogAdded: { - log.append(message); + log.append('' + message + ''); } onNameChanged: { for (let i = 0; i < buttonsModel.count; ++i) { @@ -137,7 +137,6 @@ ApplicationWindow { onStateReceived: { if (active && index == swipeView.currentIndex && !lock) { // console.log('onStateReceived', active, index, !lock); - const state = projectsModel.getProject(swipeView.currentIndex).state; listItem.stageChanged(); @@ -153,18 +152,12 @@ ApplicationWindow { console.log('no .ioc file'); } else if (state['EMPTY']) { // listView.currentItem.running = false; - Object.keys(state).forEach(key => { - for (let i = 0; i < buttonsModel.count; ++i) { - if (buttonsModel.get(i).state === key) { - if (state[key]) { - row.children[i].palette.button = 'lightgreen'; - } else { - row.children[i].palette.button = 'lightgray'; - } - break; - } + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].palette.button = 'lightgray'; + if (state[buttonsModel.get(i).state]) { + row.children[i].palette.button = 'lightgreen'; } - }); + } } } } @@ -225,26 +218,31 @@ ApplicationWindow { name: 'Initialize' state: 'INITIALIZED' action: 'save_config' + shouldRunNext: false } ListElement { name: 'Generate' state: 'GENERATED' action: 'generate_code' + shouldRunNext: false } ListElement { name: 'Initialize PlatformIO' state: 'PIO_INITIALIZED' action: 'pio_init' + shouldRunNext: false } ListElement { name: 'Patch' state: 'PATCHED' action: 'patch' + shouldRunNext: false } ListElement { name: 'Build' state: 'BUILT' action: 'build' + shouldRunNext: false } } delegate: Button { @@ -255,13 +253,14 @@ ApplicationWindow { Layout.margins: 10 // insets can be used too Layout.rightMargin: margin // rotation: -90 - function runAction() { + function runOwnAction() { listView.currentItem.actionRunning = true; + palette.button = 'gold'; const args = model.args ? model.args.split(' ') : []; listItem.run(model.action, args); } onClicked: { - runAction(); + runOwnAction(); } MouseArea { anchors.fill: parent @@ -274,8 +273,8 @@ ApplicationWindow { console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button } function shiftHandler() { - console.log('shiftHandler', shiftPressed, index); - for (let i = 2; i <= index; ++i) { + // console.log('shiftHandler', shiftPressed, index); + for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... if (shiftPressed) { if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { row.children[i].palette.button = 'honeydew'; @@ -288,13 +287,18 @@ ApplicationWindow { } } onClicked: { - parent.clicked(); // propagateComposedEvents: true // doesn't work if (ctrlPressed && model.action !== 'start_editor') { model.shouldStartEditor = true; } - if (shiftPressed) { + if (shiftPressed && index >= 2) { // run all actions in series + for (let i = 2; i < index; ++i) { + buttonsModel.setProperty(i, 'shouldRunNext', true); + } + row.children[2].clicked(); + return; } + parent.clicked(); // propagateComposedEvents doesn't work... } onPositionChanged: { if (mouse.modifiers & Qt.ControlModifier) { @@ -342,11 +346,16 @@ ApplicationWindow { glow.visible = true; seq.start(); + if (model.shouldRunNext) { + model.shouldRunNext = false; + row.children[index + 1].clicked(); // complete task + } + if (model.shouldStartEditor) { model.shouldStartEditor = false; for (let i = 0; i < buttonsModel.count; ++i) { if (buttonsModel.get(i).action === 'start_editor') { - row.children[i].runAction(); + row.children[i].runOwnAction(); // no additional actions in outer handlers break; } } @@ -361,10 +370,6 @@ ApplicationWindow { cornerRadius: 25 glowRadius: 20 spread: 0.25 - // radius: 10 - // samples: 21 - // color: 'lightgreen' - // source: actionButton } SequentialAnimation { id: seq @@ -386,7 +391,7 @@ ApplicationWindow { } } Rectangle { - width: 500 + width: 800 height: 380 ScrollView { anchors.fill: parent @@ -399,6 +404,7 @@ ApplicationWindow { selectByMouse: true wrapMode: Text.WordWrap font.family: 'Courier' + textFormat: TextEdit.RichText Component.onCompleted: listItem.completed() } } From be4884b88abaa288e79b07fd0601f55b69090dae Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 6 Feb 2020 13:55:10 +0300 Subject: [PATCH 33/54] straightforward QML-completing event --- stm32pio-gui/app.py | 4 +- stm32pio-gui/main.qml | 748 +++++++++++++++++++++--------------------- 2 files changed, 382 insertions(+), 370 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 08c316b..9d6a747 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -203,7 +203,7 @@ def init_project(self, *args, **kwargs): # import time # time.sleep(1) # if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': - # raise Exception("Error during initialization") + # raise Exception("Error during initialization") self.project = Stm32pio(*args, **kwargs) except Exception as e: self.logger.exception(e, exc_info=self.logger.isEnabledFor(logging.DEBUG)) @@ -214,7 +214,7 @@ def init_project(self, *args, **kwargs): # TODO: maybe remove _-values pass finally: - self.qml_ready.wait() # FIXME still not guaranteed, should check for ALL components to be loaded + self.qml_ready.wait() self.nameChanged.emit() self.stageChanged.emit() self.stateChanged.emit() diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 7055024..3a394a0 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -13,23 +13,22 @@ ApplicationWindow { visible: true width: 1130 height: 550 - title: "stm32pio" - color: "whitesmoke" + title: 'stm32pio' + color: 'whitesmoke' - // Popup { - // id: popup - // anchors.centerIn: parent - // modal: true + property var initInfo: ({}) + function setInitInfo(projectIndex) { + if (projectIndex in initInfo) { + initInfo[projectIndex]++; + } else { + initInfo[projectIndex] = 1; + } - // parent: Overlay.overlay - // background: Rectangle { - // color: '#00000000' - // } - // contentItem: Column { - // BusyIndicator {} - // Text { text: 'Loading...' } - // } - // } + if (initInfo[projectIndex] === 2) { + projectsModel.getProject(projectIndex).completed(); + } + // Object.keys(initInfo).forEach(key => console.log('index:', key, 'counter:', initInfo[key])); + } GridLayout { id: mainGrid @@ -38,42 +37,50 @@ ApplicationWindow { ListView { id: listView - width: 250; height: 250 + width: 250 + height: 250 model: projectsModel clip: true - delegate: Item { - id: iii - property bool loading: true - property bool actionRunning: false - width: ListView.view.width - height: 40 - property ProjectListItem listItem: projectsModel.getProject(index) - Connections { - target: listItem // sender - onNameChanged: { - loading = false; - // TODO: open the dialog where the user can enter board, editor etc. - } - onActionResult: { - actionRunning = false; + delegate: Component { + Loader { + onLoaded: { + setInitInfo(index); } - } - Row { - Column { - Text { text: 'Name: ' + display.name } - Text { text: 'Stage: ' + display.current_stage } - } - BusyIndicator { - running: iii.loading || iii.actionRunning - width: iii.height - height: iii.height - } - } - MouseArea { - anchors.fill: parent - onClicked: { - listView.currentIndex = index; - swipeView.currentIndex = index; + sourceComponent: Item { + id: iii + property bool loading: true + property bool actionRunning: false + width: listView.width + height: 40 + property ProjectListItem listItem: projectsModel.getProject(index) + Connections { + target: listItem // sender + onNameChanged: { + loading = false; + // TODO: open the dialog where the user can enter board, editor etc. + } + onActionResult: { + actionRunning = false; + } + } + Row { + Column { + Text { text: 'Name: ' + display.name } + Text { text: 'Stage: ' + display.current_stage } + } + BusyIndicator { + running: iii.loading || iii.actionRunning + width: iii.height + height: iii.height + } + } + MouseArea { + anchors.fill: parent + onClicked: { + listView.currentIndex = index; + swipeView.currentIndex = index; + } + } } } } @@ -88,365 +95,370 @@ ApplicationWindow { orientation: Qt.Vertical Repeater { model: projectsModel - delegate: Column { - property ProjectListItem listItem: projectsModel.getProject(index) - Connections { - target: listItem // sender - onLogAdded: { - log.append('' + message + ''); + delegate: Component { + Loader { + // active: SwipeView.isCurrentItem + onLoaded: { + setInitInfo(index); } - onNameChanged: { - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = true; - } - } - // Component.onCompleted: { - // for (let i = 0; i < buttonsModel.count; ++i) { - // // row.children[i].enabled = false; - // // buttonsModel.get(i).stateChangedHandler(); - // listItem.stateChanged.connect(row.children[i].haha); - // } - // } - // onStateChanged: { - // for (let i = 0; i < buttonsModel.count; ++i) { - // // row.children[i].palette.button = 'lightcoral'; - // // buttonsModel.get(i).stateChangedHandler(); - // } - // } - } - QtDialogs.MessageDialog { - id: projectIncorrectDialog - text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + - "The project will be removed from the app. It will not affect any real content" - icon: QtDialogs.StandardIcon.Critical - onAccepted: { - console.log('on accepted'); - const delIndex = swipeView.currentIndex; - listView.currentIndex = swipeView.currentIndex + 1; - swipeView.currentIndex = swipeView.currentIndex + 1; - projectsModel.removeProject(delIndex); - buttonGroup.lock = false; - } - } - ButtonGroup { - id: buttonGroup - buttons: row.children - signal stateReceived() - signal actionResult(string actionDone, bool success) - property bool lock: false - onStateReceived: { - if (active && index == swipeView.currentIndex && !lock) { - // console.log('onStateReceived', active, index, !lock); - const state = projectsModel.getProject(swipeView.currentIndex).state; - listItem.stageChanged(); - - if (state['LOADING']) { - // listView.currentItem.running = true; - } else if (state['INIT_ERROR']) { - // listView.currentItem.running = false; - row.visible = false; - initErrorMessage.visible = true; - } else if (!state['EMPTY']) { - lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) - projectIncorrectDialog.open(); - console.log('no .ioc file'); - } else if (state['EMPTY']) { - // listView.currentItem.running = false; + sourceComponent: Column { + property ProjectListItem listItem: projectsModel.getProject(index) + Connections { + target: listItem // sender + onLogAdded: { + log.append(message); // '' + } + onNameChanged: { for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].palette.button = 'lightgray'; - if (state[buttonsModel.get(i).state]) { - row.children[i].palette.button = 'lightgreen'; - } + row.children[i].enabled = true; } } - } - } - onActionResult: { - // stopActionButton.visible = false; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = true; - } - } - onClicked: { - // stopActionButton.visible = true; - // listView.currentItem.actionRunning = true; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = false; - row.children[i].glowingVisible = false; - row.children[i].anim.complete(); - // if (buttonsModel.get(i).name === button.text) { - // const b = buttonsModel.get(i); - // const args = b.args ? b.args.split(' ') : []; - // listItem.run(b.action, args); + // Component.onCompleted: { + // for (let i = 0; i < buttonsModel.count; ++i) { + // // row.children[i].enabled = false; + // // buttonsModel.get(i).stateChangedHandler(); + // listItem.stateChanged.connect(row.children[i].haha); + // } + // } + // onStateChanged: { + // for (let i = 0; i < buttonsModel.count; ++i) { + // // row.children[i].palette.button = 'lightcoral'; + // // buttonsModel.get(i).stateChangedHandler(); + // } // } } - } - Component.onCompleted: { - listItem.stateChanged.connect(stateReceived); - swipeView.currentItemChanged.connect(stateReceived); - mainWindow.activeChanged.connect(stateReceived); - - listItem.actionResult.connect(actionResult); - } - } - Text { - id: initErrorMessage - visible: false - padding: 10 - text: "The project cannot be initialized" - color: 'red' - } - RowLayout { - id: row - // padding: 10 - // spacing: 10 - z: 1 - Repeater { - model: ListModel { - id: buttonsModel - ListElement { - name: 'Clean' - action: 'clean' + QtDialogs.MessageDialog { + id: projectIncorrectDialog + text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + + "The project will be removed from the app. It will not affect any real content" + icon: QtDialogs.StandardIcon.Critical + onAccepted: { + console.log('on accepted'); + const delIndex = swipeView.currentIndex; + listView.currentIndex = swipeView.currentIndex + 1; + swipeView.currentIndex = swipeView.currentIndex + 1; + projectsModel.removeProject(delIndex); + buttonGroup.lock = false; } - ListElement { - name: 'Open editor' - action: 'start_editor' - args: 'code' - margin: 15 // margin to visually separate the Clean action as it doesn't represent any state - } - ListElement { - name: 'Initialize' - state: 'INITIALIZED' - action: 'save_config' - shouldRunNext: false - } - ListElement { - name: 'Generate' - state: 'GENERATED' - action: 'generate_code' - shouldRunNext: false + } + ButtonGroup { + id: buttonGroup + buttons: row.children + signal stateReceived() + signal actionResult(string actionDone, bool success) + property bool lock: false + onStateReceived: { + if (active && index == swipeView.currentIndex && !lock) { + // console.log('onStateReceived', active, index, !lock); + const state = projectsModel.getProject(swipeView.currentIndex).state; + listItem.stageChanged(); + + if (state['LOADING']) { + // listView.currentItem.running = true; + } else if (state['INIT_ERROR']) { + // listView.currentItem.running = false; + row.visible = false; + initErrorMessage.visible = true; + } else if (!state['EMPTY']) { + lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) + projectIncorrectDialog.open(); + console.log('no .ioc file'); + } else if (state['EMPTY']) { + // listView.currentItem.running = false; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].palette.button = 'lightgray'; + if (state[buttonsModel.get(i).state]) { + row.children[i].palette.button = 'lightgreen'; + } + } + } + } } - ListElement { - name: 'Initialize PlatformIO' - state: 'PIO_INITIALIZED' - action: 'pio_init' - shouldRunNext: false + onActionResult: { + // stopActionButton.visible = false; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = true; + } } - ListElement { - name: 'Patch' - state: 'PATCHED' - action: 'patch' - shouldRunNext: false + onClicked: { + // stopActionButton.visible = true; + // listView.currentItem.actionRunning = true; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = false; + row.children[i].glowingVisible = false; + row.children[i].anim.complete(); + // if (buttonsModel.get(i).name === button.text) { + // const b = buttonsModel.get(i); + // const args = b.args ? b.args.split(' ') : []; + // listItem.run(b.action, args); + // } + } } - ListElement { - name: 'Build' - state: 'BUILT' - action: 'build' - shouldRunNext: false + Component.onCompleted: { + listItem.stateChanged.connect(stateReceived); + swipeView.currentItemChanged.connect(stateReceived); + mainWindow.activeChanged.connect(stateReceived); + + listItem.actionResult.connect(actionResult); } } - delegate: Button { - text: name - enabled: false - property alias glowingVisible: glow.visible - property alias anim: seq - Layout.margins: 10 // insets can be used too - Layout.rightMargin: margin - // rotation: -90 - function runOwnAction() { - listView.currentItem.actionRunning = true; - palette.button = 'gold'; - const args = model.args ? model.args.split(' ') : []; - listItem.run(model.action, args); - } - onClicked: { - runOwnAction(); - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - property bool ctrlPressed: false - property bool ctrlPressedLastState: false - property bool shiftPressed: false - property bool shiftPressedLastState: false - function h() { - console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button - } - function shiftHandler() { - // console.log('shiftHandler', shiftPressed, index); - for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... - if (shiftPressed) { - if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { - row.children[i].palette.button = 'honeydew'; - } - } else { - if (Qt.colorEqual(row.children[i].palette.button, 'honeydew')) { - row.children[i].palette.button = 'lightgray'; - } - } + Text { + id: initErrorMessage + visible: false + padding: 10 + text: "The project cannot be initialized" + color: 'red' + } + RowLayout { + id: row + // padding: 10 + // spacing: 10 + z: 1 + Repeater { + model: ListModel { + id: buttonsModel + ListElement { + name: 'Clean' + action: 'clean' } - } - onClicked: { - if (ctrlPressed && model.action !== 'start_editor') { - model.shouldStartEditor = true; + ListElement { + name: 'Open editor' + action: 'start_editor' + args: 'code' + margin: 15 // margin to visually separate the Clean action as it doesn't represent any state } - if (shiftPressed && index >= 2) { - // run all actions in series - for (let i = 2; i < index; ++i) { - buttonsModel.setProperty(i, 'shouldRunNext', true); - } - row.children[2].clicked(); - return; + ListElement { + name: 'Initialize' + state: 'INITIALIZED' + action: 'save_config' + shouldRunNext: false } - parent.clicked(); // propagateComposedEvents doesn't work... - } - onPositionChanged: { - if (mouse.modifiers & Qt.ControlModifier) { - ctrlPressed = true; - } else { - ctrlPressed = false; + ListElement { + name: 'Generate' + state: 'GENERATED' + action: 'generate_code' + shouldRunNext: false } - if (ctrlPressedLastState !== ctrlPressed) { - ctrlPressedLastState = ctrlPressed; - h(); + ListElement { + name: 'Initialize PlatformIO' + state: 'PIO_INITIALIZED' + action: 'pio_init' + shouldRunNext: false } - - if (mouse.modifiers & Qt.ShiftModifier) { - shiftPressed = true; - } else { - shiftPressed = false; + ListElement { + name: 'Patch' + state: 'PATCHED' + action: 'patch' + shouldRunNext: false } - if (shiftPressedLastState !== shiftPressed) { - shiftPressedLastState = shiftPressed; - shiftHandler(); + ListElement { + name: 'Build' + state: 'BUILT' + action: 'build' + shouldRunNext: false } } - onExited: { - ctrlPressed = false; - ctrlPressedLastState = false; - - if (shiftPressed || shiftPressedLastState) { - shiftPressed = false; - shiftPressedLastState = false; - shiftHandler(); + delegate: Button { + text: name + enabled: false + property alias glowingVisible: glow.visible + property alias anim: seq + Layout.margins: 10 // insets can be used too + Layout.rightMargin: margin + // rotation: -90 + function runOwnAction() { + listView.currentItem.item.actionRunning = true; + palette.button = 'gold'; + const args = model.args ? model.args.split(' ') : []; + listItem.run(model.action, args); } - } - } - Connections { - target: buttonGroup - onActionResult: { - // console.log('actionDone', actionDone, model.name); - if (actionDone === model.action) { - if (success) { - glow.color = 'lightgreen'; - } else { - palette.button = 'lightcoral'; - glow.color = 'lightcoral'; + onClicked: { + runOwnAction(); + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + property bool ctrlPressed: false + property bool ctrlPressedLastState: false + property bool shiftPressed: false + property bool shiftPressedLastState: false + function h() { + console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button + } + function shiftHandler() { + // console.log('shiftHandler', shiftPressed, index); + for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... + if (shiftPressed) { + if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { + row.children[i].palette.button = 'honeydew'; + } + } else { + if (Qt.colorEqual(row.children[i].palette.button, 'honeydew')) { + row.children[i].palette.button = 'lightgray'; + } + } + } + } + onClicked: { + if (ctrlPressed && model.action !== 'start_editor') { + model.shouldStartEditor = true; + } + if (shiftPressed && index >= 2) { + // run all actions in series + for (let i = 2; i < index; ++i) { + buttonsModel.setProperty(i, 'shouldRunNext', true); + } + row.children[2].clicked(); + return; + } + parent.clicked(); // propagateComposedEvents doesn't work... } - glow.visible = true; - seq.start(); + onPositionChanged: { + if (mouse.modifiers & Qt.ControlModifier) { + ctrlPressed = true; + } else { + ctrlPressed = false; + } + if (ctrlPressedLastState !== ctrlPressed) { + ctrlPressedLastState = ctrlPressed; + h(); + } - if (model.shouldRunNext) { - model.shouldRunNext = false; - row.children[index + 1].clicked(); // complete task + if (mouse.modifiers & Qt.ShiftModifier) { + shiftPressed = true; + } else { + shiftPressed = false; + } + if (shiftPressedLastState !== shiftPressed) { + shiftPressedLastState = shiftPressed; + shiftHandler(); + } } + onExited: { + ctrlPressed = false; + ctrlPressedLastState = false; - if (model.shouldStartEditor) { - model.shouldStartEditor = false; - for (let i = 0; i < buttonsModel.count; ++i) { - if (buttonsModel.get(i).action === 'start_editor') { - row.children[i].runOwnAction(); // no additional actions in outer handlers - break; + if (shiftPressed || shiftPressedLastState) { + shiftPressed = false; + shiftPressedLastState = false; + shiftHandler(); + } + } + } + Connections { + target: buttonGroup + onActionResult: { + // console.log('actionDone', actionDone, model.name); + if (actionDone === model.action) { + if (success) { + glow.color = 'lightgreen'; + } else { + palette.button = 'lightcoral'; + glow.color = 'lightcoral'; + } + glow.visible = true; + seq.start(); + + if (model.shouldRunNext) { + model.shouldRunNext = false; + row.children[index + 1].clicked(); // complete task + } + + if (model.shouldStartEditor) { + model.shouldStartEditor = false; + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).action === 'start_editor') { + row.children[i].runOwnAction(); // no additional actions in outer handlers + break; + } + } } } } } + RectangularGlow { + id: glow + visible: false + anchors.fill: parent + cornerRadius: 25 + glowRadius: 20 + spread: 0.25 + } + SequentialAnimation { + id: seq + loops: 3 + OpacityAnimator { + target: glow + from: 0 + to: 1 + duration: 1000 + } + OpacityAnimator { + target: glow + from: 1 + to: 0 + duration: 1000 + } + } } } - RectangularGlow { - id: glow - visible: false + } + Rectangle { + width: 800 + height: 380 + ScrollView { anchors.fill: parent - cornerRadius: 25 - glowRadius: 20 - spread: 0.25 + TextArea { + id: log + // anchors.fill: parent + width: 500 + height: 380 + readOnly: true + selectByMouse: true + wrapMode: Text.WordWrap + font.family: 'Courier' + // textFormat: TextEdit.RichText + // Component.onCompleted: console.log('textArea completed'); + } + } + } + // Button { + // text: 'test' + // onClicked: { + // row.visible = false; + // } + // } + // Button { + // id: stopActionButton + // text: 'Stop' + // visible: false + // palette.button: 'lightcoral' + // onClicked: { + // // projectIncorrectDialog.open(); + // console.log(listItem.stop('generate_code')); + // } + // } + Column { + id: initDialog + // visible: false + Text { + text: 'You can specify blabla' } - SequentialAnimation { - id: seq - loops: 3 - OpacityAnimator { - target: glow - from: 0 - to: 1 - duration: 1000 + Row { + TextField { + placeholderText: 'Board' } - OpacityAnimator { - target: glow - from: 1 - to: 0 - duration: 1000 + TextField { + placeholderText: 'Editor' + } + CheckBox { + text: 'Build' + enabled: false } } } } } - Rectangle { - width: 800 - height: 380 - ScrollView { - anchors.fill: parent - TextArea { - id: log - // anchors.fill: parent - width: 500 - height: 380 - readOnly: true - selectByMouse: true - wrapMode: Text.WordWrap - font.family: 'Courier' - textFormat: TextEdit.RichText - Component.onCompleted: listItem.completed() - } - } - } - // Text { - // text: 'Name: ' + display.name - // } - // Button { - // text: 'test' - // onClicked: { - // row.visible = false; - // } - // } - // Button { - // id: stopActionButton - // text: 'Stop' - // visible: false - // palette.button: 'lightcoral' - // onClicked: { - // // projectIncorrectDialog.open(); - // console.log(listItem.stop('generate_code')); - // } - // } - Column { - id: initDialog - // visible: false - Text { - text: 'You can specify blabla' - } - Row { - TextField { - placeholderText: 'Board' - } - TextField { - placeholderText: 'Editor' - } - CheckBox { - text: 'Build' - enabled: false - } - } - } } } } From d8e0f5c7bb29a22d5c7b1bb4b146539d26932931 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 6 Feb 2020 22:36:11 +0300 Subject: [PATCH 34/54] log formatting in QML depending on level --- stm32pio-gui/app.py | 16 ++++++++++++---- stm32pio-gui/main.qml | 11 +++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 9d6a747..0b0ae9c 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -103,12 +103,12 @@ def emit(self, record: logging.LogRecord) -> None: self.temp_logs.reverse() for i in range(len(self.temp_logs)): m = self.temp_logs.pop() - self.signal.emit(m) - self.signal.emit(msg) + self.signal.emit(m, record.levelno) + self.signal.emit(msg, record.levelno) class HandlerWorker(QObject): - addLog = Signal(str) + addLog = Signal(str, int) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -147,7 +147,7 @@ class ProjectListItem(QObject): nameChanged = Signal() stateChanged = Signal() stageChanged = Signal() - logAdded = Signal(str, arguments=['message']) + logAdded = Signal(str, int, arguments=['message', 'level']) actionResult = Signal(str, bool, arguments=['action', 'success']) # ccompleted = Signal() @@ -407,6 +407,14 @@ def qt_message_handler(mode, context, message): # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) engine.rootContext().setContextProperty('projectsModel', projects) + engine.rootContext().setContextProperty('Logging', { + 'CRITICAL': logging.CRITICAL, + 'ERROR': logging.ERROR, + 'WARNING': logging.WARNING, + 'INFO': logging.INFO, + 'DEBUG': logging.DEBUG, + 'NOTSET': logging.NOTSET + }) engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml')) # engine.quit.connect(app.quit) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 3a394a0..cd6b34a 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -106,7 +106,13 @@ ApplicationWindow { Connections { target: listItem // sender onLogAdded: { - log.append(message); // '' + if (level === Logging.WARNING) { + log.append('
' + message + '
'); + } else if (level >= Logging.ERROR) { + log.append('
' + message + '
'); + } else { + log.append('
' + message + '
'); + } } onNameChanged: { for (let i = 0; i < buttonsModel.count; ++i) { @@ -417,7 +423,8 @@ ApplicationWindow { selectByMouse: true wrapMode: Text.WordWrap font.family: 'Courier' - // textFormat: TextEdit.RichText + font.pointSize: 10 + textFormat: TextEdit.RichText // Component.onCompleted: console.log('textArea completed'); } } From 9425d24d33ac65c170425ea7a4ebe026b41bd115 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 9 Feb 2020 01:16:52 +0300 Subject: [PATCH 35/54] project actions queue, QML huge work --- stm32pio-gui/app.py | 76 ++-- stm32pio-gui/main.qml | 824 ++++++++++++++++++++++++------------------ stm32pio/lib.py | 4 +- 3 files changed, 511 insertions(+), 393 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 0b0ae9c..3ceb10f 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -15,7 +15,7 @@ from PySide2.QtCore import QCoreApplication, QUrl, QAbstractItemModel, Property, QAbstractListModel, QModelIndex, \ QObject, Qt, Slot, Signal, QTimer, QThread, qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, \ - QtFatalMsg + QtFatalMsg, QThreadPool, QRunnable from PySide2.QtGui import QGuiApplication from PySide2.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine from PySide2.QtQuick import QQuickView @@ -139,7 +139,11 @@ def __init__(self, *args, **kwargs): class Stm32pio(stm32pio.lib.Stm32pio): - def save_config(self): + def save_config(self, parameters: dict = None): + if parameters is not None: + for section_name, section_value in parameters.items(): + for key, value in section_value.items(): + self.config.set(section_name, key, value) self.config.save() @@ -149,27 +153,29 @@ class ProjectListItem(QObject): stageChanged = Signal() logAdded = Signal(str, int, arguments=['message', 'level']) actionResult = Signal(str, bool, arguments=['action', 'success']) - # ccompleted = Signal() def __init__(self, project_args: list = None, project_kwargs: dict = None, parent: QObject = None): - QObject.__init__(self, parent=parent) + super().__init__(parent=parent) self.logThread = QThread() # TODO: can be a 'daemon' type as it runs alongside the main for a long time self.handler = HandlerWorker() self.handler.moveToThread(self.logThread) self.handler.addLog.connect(self.logAdded) - # self.ccompleted.connect(self.handler.cccompleted) self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") self.logger.addHandler(self.handler.logging_handler) - self.logger.setLevel(logging.DEBUG) + self.logger.setLevel(logging.INFO) self.handler.logging_handler.setFormatter(stm32pio.util.DispatchingFormatter( f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", special=special_formatters)) self.logThread.start() - self.worker = ProjectActionWorker(self.logger, lambda: None) + self.workers_pool = QThreadPool() + self.workers_pool.setMaxThreadCount(1) + self.workers_pool.setExpiryTimeout(-1) + + # self.worker = ProjectActionWorker(self.logger, lambda: None) self.project = None self._name = 'Loading...' @@ -218,7 +224,6 @@ def init_project(self, *args, **kwargs): self.nameChanged.emit() self.stageChanged.emit() self.stateChanged.emit() - # self.worker = ProjectActionWorker(self.logger, job) def at_exit(self): print('destroy', self) @@ -235,7 +240,7 @@ def name(self): @Property('QVariant', notify=stateChanged) def state(self): if self.project is not None: - return { s.name: value for s, value in self.project.state.items() } + return { s.name: value for s, value in self.project.state.items() if s != stm32pio.lib.ProjectStage.UNDEFINED } else: return self._state @@ -247,10 +252,6 @@ def current_stage(self): else: return self._current_stage - # @Slot(result=bool) - # def is_present(self): - # return self.state[stm32pio.lib.ProjectStage.EMPTY] - @Slot() def completed(self): print('completed from QML') @@ -261,30 +262,43 @@ def completed(self): @Slot(str, 'QVariantList') def run(self, action, args): # TODO: queue or smth of jobs - self.worker = ProjectActionWorker(self.logger, getattr(self.project, action), args) + worker = NewProjectActionWorker(self.logger, getattr(self.project, action), args) + worker.actionResult.connect(self.stateChanged) + worker.actionResult.connect(self.stageChanged) + worker.actionResult.connect(self.actionResult) + + self.workers_pool.start(worker) - self.worker.actionResult.connect(self.stateChanged) - self.worker.actionResult.connect(self.stageChanged) - self.worker.actionResult.connect(self.actionResult) - # @Slot(str) - # def stop(self, action): - # if self.worker.thread.isRunning() and self.worker.name == action: - # print('===============================', self.worker.thread.quit()) +class NewProjectActionWorker(QObject, QRunnable): + actionResult = Signal(str, bool, arguments=['action', 'success']) + def __init__(self, logger, func, args=None): + QObject.__init__(self, parent=None) + QRunnable.__init__(self) - # def save_config(self): - # return self.config.save() + self.logger = logger + self.func = func + if args is None: + self.args = [] + else: + self.args = args + self.name = func.__name__ - # this = super() - # class Worker(QThread): - # def run(self): - # this.generate_code() - # self.w = Worker() - # self.w.start() + 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.actionResult.emit(self.name, success) - # return super().generate_code() class ProjectActionWorker(QObject): @@ -350,7 +364,7 @@ def add(self, project): def addProject(self, path): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) project = ProjectListItem(project_args=[path.toLocalFile()], - project_kwargs=dict(save_on_destruction=False, parameters={'board': 'nucleo_f031k6'})) + project_kwargs=dict(save_on_destruction=False)) # project = ProjectListItem() self.projects.append(project) self.endInsertRows() diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index cd6b34a..843c86c 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -26,6 +26,13 @@ ApplicationWindow { if (initInfo[projectIndex] === 2) { projectsModel.getProject(projectIndex).completed(); + // const indexToOpen = listView.indexToOpenAfterAddition; + // console.log('indexToOpen', indexToOpen); + // if (indexToOpen !== -1) { + // listView.indexToOpenAfterAddition = -1; + // listView.currentIndex = indexToOpen; + // swipeView.currentIndex = indexToOpen; + // } } // Object.keys(initInfo).forEach(key => console.log('index:', key, 'counter:', initInfo[key])); } @@ -35,57 +42,74 @@ ApplicationWindow { columns: 2 rows: 2 - ListView { - id: listView - width: 250 - height: 250 - model: projectsModel - clip: true - delegate: Component { - Loader { - onLoaded: { - setInitInfo(index); - } - sourceComponent: Item { - id: iii - property bool loading: true - property bool actionRunning: false - width: listView.width - height: 40 - property ProjectListItem listItem: projectsModel.getProject(index) - Connections { - target: listItem // sender - onNameChanged: { - loading = false; - // TODO: open the dialog where the user can enter board, editor etc. - } - onActionResult: { - actionRunning = false; - } + Column { + ListView { + id: listView + width: 250 + height: 250 + model: projectsModel + clip: true + property int indexToOpenAfterAddition: -1 + highlight: Rectangle { color: "lightsteelblue"; radius: 5 } + // focus: true + delegate: Component { + Loader { + onLoaded: { + setInitInfo(index); } - Row { - Column { - Text { text: 'Name: ' + display.name } - Text { text: 'Stage: ' + display.current_stage } + sourceComponent: Item { + id: iii + property bool loading: true + property bool actionRunning: false + width: listView.width + height: 40 + property ProjectListItem listItem: projectsModel.getProject(index) + Connections { + target: listItem // sender + onNameChanged: { + loading = false; + } + onActionResult: { + actionRunning = false; + } } - BusyIndicator { - running: iii.loading || iii.actionRunning - width: iii.height - height: iii.height + Row { + Column { + Text { text: 'Name: ' + display.name } + Text { text: 'Stage: ' + display.current_stage } + } + BusyIndicator { + running: iii.loading || iii.actionRunning + width: iii.height + height: iii.height + } } - } - MouseArea { - anchors.fill: parent - onClicked: { - listView.currentIndex = index; - swipeView.currentIndex = index; + MouseArea { + anchors.fill: parent + enabled: !parent.loading + onClicked: { + listView.currentIndex = index; + swipeView.currentIndex = index; + } } } } } } - highlight: Rectangle { color: "lightsteelblue"; radius: 5 } - // focus: true + QtLabs.FolderDialog { + id: folderDialog + currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] + onAccepted: { + listView.indexToOpenAfterAddition = listView.count; + projectsModel.addProject(folder); + } + } + Button { + text: 'Add' + onClicked: { + folderDialog.open(); + } + } } SwipeView { @@ -118,349 +142,439 @@ ApplicationWindow { for (let i = 0; i < buttonsModel.count; ++i) { row.children[i].enabled = true; } + + const state = listItem.state; + const s = Object.keys(state).filter(stateName => state[stateName]); + if (s.length === 1 && s[0] === 'EMPTY') { + initDialogLoader.active = true; + } else { + content.visible = true; + } } - // Component.onCompleted: { - // for (let i = 0; i < buttonsModel.count; ++i) { - // // row.children[i].enabled = false; - // // buttonsModel.get(i).stateChangedHandler(); - // listItem.stateChanged.connect(row.children[i].haha); - // } - // } - // onStateChanged: { - // for (let i = 0; i < buttonsModel.count; ++i) { - // // row.children[i].palette.button = 'lightcoral'; - // // buttonsModel.get(i).stateChangedHandler(); - // } - // } } - QtDialogs.MessageDialog { - id: projectIncorrectDialog - text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + - "The project will be removed from the app. It will not affect any real content" - icon: QtDialogs.StandardIcon.Critical - onAccepted: { - console.log('on accepted'); - const delIndex = swipeView.currentIndex; - listView.currentIndex = swipeView.currentIndex + 1; - swipeView.currentIndex = swipeView.currentIndex + 1; - projectsModel.removeProject(delIndex); - buttonGroup.lock = false; + Column { + id: content + visible: false + QtDialogs.MessageDialog { + id: projectIncorrectDialog + text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + + "The project will be removed from the app. It will not affect any real content" + icon: QtDialogs.StandardIcon.Critical + onAccepted: { + console.log('on accepted'); + const delIndex = swipeView.currentIndex; + listView.currentIndex = swipeView.currentIndex + 1; + swipeView.currentIndex = swipeView.currentIndex + 1; + projectsModel.removeProject(delIndex); + buttonGroup.lock = false; + } } - } - ButtonGroup { - id: buttonGroup - buttons: row.children - signal stateReceived() - signal actionResult(string actionDone, bool success) - property bool lock: false - onStateReceived: { - if (active && index == swipeView.currentIndex && !lock) { - // console.log('onStateReceived', active, index, !lock); - const state = projectsModel.getProject(swipeView.currentIndex).state; - listItem.stageChanged(); + ButtonGroup { + id: buttonGroup + buttons: row.children + signal stateReceived() + signal actionResult(string actionDone, bool success) + property bool lock: false + onStateReceived: { + if (active && index == swipeView.currentIndex && !lock) { + // console.log('onStateReceived', active, index, !lock); + const state = listItem.state; + listItem.stageChanged(); - if (state['LOADING']) { - // listView.currentItem.running = true; - } else if (state['INIT_ERROR']) { - // listView.currentItem.running = false; - row.visible = false; - initErrorMessage.visible = true; - } else if (!state['EMPTY']) { - lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) - projectIncorrectDialog.open(); - console.log('no .ioc file'); - } else if (state['EMPTY']) { - // listView.currentItem.running = false; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].palette.button = 'lightgray'; - if (state[buttonsModel.get(i).state]) { - row.children[i].palette.button = 'lightgreen'; + if (state['LOADING']) { + // listView.currentItem.running = true; + } else if (state['INIT_ERROR']) { + // listView.currentItem.running = false; + row.visible = false; + initErrorMessage.visible = true; + } else if (!state['EMPTY']) { + lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) + projectIncorrectDialog.open(); + console.log('no .ioc file'); + } else if (state['EMPTY']) { + // listView.currentItem.running = false; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].palette.button = 'lightgray'; + if (state[buttonsModel.get(i).state]) { + row.children[i].palette.button = 'lightgreen'; + } } } } } - } - onActionResult: { - // stopActionButton.visible = false; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = true; + onActionResult: { + // stopActionButton.visible = false; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = true; + } } - } - onClicked: { - // stopActionButton.visible = true; - // listView.currentItem.actionRunning = true; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = false; - row.children[i].glowingVisible = false; - row.children[i].anim.complete(); - // if (buttonsModel.get(i).name === button.text) { - // const b = buttonsModel.get(i); - // const args = b.args ? b.args.split(' ') : []; - // listItem.run(b.action, args); - // } + onClicked: { + // stopActionButton.visible = true; + // listView.currentItem.actionRunning = true; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = false; + row.children[i].glowingVisible = false; + row.children[i].anim.complete(); + // if (buttonsModel.get(i).name === button.text) { + // const b = buttonsModel.get(i); + // const args = b.args ? b.args.split(' ') : []; + // listItem.run(b.action, args); + // } + } } - } - Component.onCompleted: { - listItem.stateChanged.connect(stateReceived); - swipeView.currentItemChanged.connect(stateReceived); - mainWindow.activeChanged.connect(stateReceived); + Component.onCompleted: { + listItem.stateChanged.connect(stateReceived); + swipeView.currentItemChanged.connect(stateReceived); + mainWindow.activeChanged.connect(stateReceived); - listItem.actionResult.connect(actionResult); - } - } - Text { - id: initErrorMessage - visible: false - padding: 10 - text: "The project cannot be initialized" - color: 'red' - } - RowLayout { - id: row - // padding: 10 - // spacing: 10 - z: 1 - Repeater { - model: ListModel { - id: buttonsModel - ListElement { - name: 'Clean' - action: 'clean' - } - ListElement { - name: 'Open editor' - action: 'start_editor' - args: 'code' - margin: 15 // margin to visually separate the Clean action as it doesn't represent any state - } - ListElement { - name: 'Initialize' - state: 'INITIALIZED' - action: 'save_config' - shouldRunNext: false - } - ListElement { - name: 'Generate' - state: 'GENERATED' - action: 'generate_code' - shouldRunNext: false - } - ListElement { - name: 'Initialize PlatformIO' - state: 'PIO_INITIALIZED' - action: 'pio_init' - shouldRunNext: false - } - ListElement { - name: 'Patch' - state: 'PATCHED' - action: 'patch' - shouldRunNext: false - } - ListElement { - name: 'Build' - state: 'BUILT' - action: 'build' - shouldRunNext: false - } + listItem.actionResult.connect(actionResult); } - delegate: Button { - text: name - enabled: false - property alias glowingVisible: glow.visible - property alias anim: seq - Layout.margins: 10 // insets can be used too - Layout.rightMargin: margin - // rotation: -90 - function runOwnAction() { - listView.currentItem.item.actionRunning = true; - palette.button = 'gold'; - const args = model.args ? model.args.split(' ') : []; - listItem.run(model.action, args); - } - onClicked: { - runOwnAction(); + } + Text { + id: initErrorMessage + visible: false + padding: 10 + text: "The project cannot be initialized" + color: 'red' + } + RowLayout { + id: row + // padding: 10 + // spacing: 10 + z: 1 + Repeater { + model: ListModel { + id: buttonsModel + ListElement { + name: 'Clean' + action: 'clean' + shouldStartEditor: false + } + ListElement { + name: 'Open editor' + action: 'start_editor' + args: 'code' + margin: 15 // margin to visually separate the Clean action as it doesn't represent any state + } + ListElement { + name: 'Initialize' + state: 'INITIALIZED' + action: 'save_config' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Generate' + state: 'GENERATED' + action: 'generate_code' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Initialize PlatformIO' + state: 'PIO_INITIALIZED' + action: 'pio_init' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Patch' + state: 'PATCHED' + action: 'patch' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Build' + state: 'BUILT' + action: 'build' + shouldRunNext: false + shouldStartEditor: false + } } - MouseArea { - anchors.fill: parent - hoverEnabled: true - property bool ctrlPressed: false - property bool ctrlPressedLastState: false - property bool shiftPressed: false - property bool shiftPressedLastState: false - function h() { - console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button + delegate: Button { + text: name + enabled: false + property alias glowingVisible: glow.visible + property alias anim: seq + Layout.margins: 10 // insets can be used too + Layout.rightMargin: margin + // rotation: -90 + function runOwnAction() { + listView.currentItem.item.actionRunning = true; + palette.button = 'gold'; + const args = model.args ? model.args.split(' ') : []; + listItem.run(model.action, args); } - function shiftHandler() { - // console.log('shiftHandler', shiftPressed, index); - for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... - if (shiftPressed) { - if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { - row.children[i].palette.button = 'honeydew'; - } - } else { - if (Qt.colorEqual(row.children[i].palette.button, 'honeydew')) { - row.children[i].palette.button = 'lightgray'; + onClicked: { + runOwnAction(); + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + property bool ctrlPressed: false + property bool ctrlPressedLastState: false + property bool shiftPressed: false + property bool shiftPressedLastState: false + // function h() { + // console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button + // } + function shiftHandler() { + // console.log('shiftHandler', shiftPressed, index); + for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... + if (shiftPressed) { + // if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { + row.children[i].palette.button = 'honeydew'; + // } + } else { + buttonGroup.stateReceived(); + // if (Qt.colorEqual(row.children[i].palette.button, 'honeydew')) { + // row.children[i].palette.button = 'lightgray'; + // } } } } - } - onClicked: { - if (ctrlPressed && model.action !== 'start_editor') { - model.shouldStartEditor = true; - } - if (shiftPressed && index >= 2) { - // run all actions in series - for (let i = 2; i < index; ++i) { - buttonsModel.setProperty(i, 'shouldRunNext', true); + onClicked: { + if (ctrlPressed && model.action !== 'start_editor') { + model.shouldStartEditor = true; } - row.children[2].clicked(); - return; - } - parent.clicked(); // propagateComposedEvents doesn't work... - } - onPositionChanged: { - if (mouse.modifiers & Qt.ControlModifier) { - ctrlPressed = true; - } else { - ctrlPressed = false; - } - if (ctrlPressedLastState !== ctrlPressed) { - ctrlPressedLastState = ctrlPressed; - h(); + if (shiftPressed && index >= 2) { + // run all actions in series + for (let i = 2; i < index; ++i) { + buttonsModel.setProperty(i, 'shouldRunNext', true); + } + row.children[2].clicked(); + return; + } + parent.clicked(); // propagateComposedEvents doesn't work... } + onPositionChanged: { + if (mouse.modifiers & Qt.ControlModifier) { + ctrlPressed = true; + } else { + ctrlPressed = false; + } + if (ctrlPressedLastState !== ctrlPressed) { + ctrlPressedLastState = ctrlPressed; + } - if (mouse.modifiers & Qt.ShiftModifier) { - shiftPressed = true; - } else { - shiftPressed = false; + if (mouse.modifiers & Qt.ShiftModifier) { + shiftPressed = true; + } else { + shiftPressed = false; + } + if (shiftPressedLastState !== shiftPressed) { + shiftPressedLastState = shiftPressed; + shiftHandler(); + } } - if (shiftPressedLastState !== shiftPressed) { - shiftPressedLastState = shiftPressed; - shiftHandler(); + onEntered: { + statusBar.text = 'Ctrl-click to open the editor specified in the Settings after the operation, Shift-click to perform all actions prior this one (including). Ctrl-Shift-click for both'; } - } - onExited: { - ctrlPressed = false; - ctrlPressedLastState = false; + onExited: { + statusBar.text = ''; + + ctrlPressed = false; + ctrlPressedLastState = false; - if (shiftPressed || shiftPressedLastState) { - shiftPressed = false; - shiftPressedLastState = false; - shiftHandler(); + if (shiftPressed || shiftPressedLastState) { + shiftPressed = false; + shiftPressedLastState = false; + shiftHandler(); + } } } - } - Connections { - target: buttonGroup - onActionResult: { - // console.log('actionDone', actionDone, model.name); - if (actionDone === model.action) { - if (success) { - glow.color = 'lightgreen'; - } else { - palette.button = 'lightcoral'; - glow.color = 'lightcoral'; - } - glow.visible = true; - seq.start(); + Connections { + target: buttonGroup + onActionResult: { + // console.log('actionDone', actionDone, model.name); + if (actionDone === model.action) { + if (success) { + glow.color = 'lightgreen'; + } else { + palette.button = 'lightcoral'; + glow.color = 'lightcoral'; + } + glow.visible = true; + seq.start(); - if (model.shouldRunNext) { - model.shouldRunNext = false; - row.children[index + 1].clicked(); // complete task - } + if (model.shouldRunNext) { + model.shouldRunNext = false; + row.children[index + 1].clicked(); // complete task + } - if (model.shouldStartEditor) { - model.shouldStartEditor = false; - for (let i = 0; i < buttonsModel.count; ++i) { - if (buttonsModel.get(i).action === 'start_editor') { - row.children[i].runOwnAction(); // no additional actions in outer handlers - break; + if (model.shouldStartEditor) { + model.shouldStartEditor = false; + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).action === 'start_editor') { + row.children[i].runOwnAction(); // no additional actions in outer handlers + break; + } } } } } } - } - RectangularGlow { - id: glow - visible: false - anchors.fill: parent - cornerRadius: 25 - glowRadius: 20 - spread: 0.25 - } - SequentialAnimation { - id: seq - loops: 3 - OpacityAnimator { - target: glow - from: 0 - to: 1 - duration: 1000 + RectangularGlow { + id: glow + visible: false + anchors.fill: parent + cornerRadius: 25 + glowRadius: 20 + spread: 0.25 } - OpacityAnimator { - target: glow - from: 1 - to: 0 - duration: 1000 + SequentialAnimation { + id: seq + loops: 3 + OpacityAnimator { + target: glow + from: 0 + to: 1 + duration: 1000 + } + OpacityAnimator { + target: glow + from: 1 + to: 0 + duration: 1000 + } } } } } - } - Rectangle { - width: 800 - height: 380 - ScrollView { - anchors.fill: parent - TextArea { - id: log - // anchors.fill: parent - width: 500 - height: 380 - readOnly: true - selectByMouse: true - wrapMode: Text.WordWrap - font.family: 'Courier' - font.pointSize: 10 - textFormat: TextEdit.RichText - // Component.onCompleted: console.log('textArea completed'); + Rectangle { + width: 800 + height: 380 + ScrollView { + anchors.fill: parent + TextArea { + id: log + // anchors.fill: parent + width: 500 + height: 380 + readOnly: true + selectByMouse: true + wrapMode: Text.WordWrap + font.family: 'Courier' + font.pointSize: 10 + textFormat: TextEdit.RichText + // Component.onCompleted: console.log('textArea completed'); + } } } } - // Button { - // text: 'test' - // onClicked: { - // row.visible = false; - // } - // } - // Button { - // id: stopActionButton - // text: 'Stop' - // visible: false - // palette.button: 'lightcoral' - // onClicked: { - // // projectIncorrectDialog.open(); - // console.log(listItem.stop('generate_code')); - // } - // } - Column { - id: initDialog - // visible: false - Text { - text: 'You can specify blabla' - } - Row { - TextField { - placeholderText: 'Board' + Loader { + id: initDialogLoader + active: false + sourceComponent: Column { + Text { + text: 'To complete initialization you can provide PlatformIO name of the board' } - TextField { - placeholderText: 'Editor' + Row { + ComboBox { + id: board + editable: true + model: ListModel { + ListElement { text: "None" } + ListElement { text: "Banana" } + ListElement { text: "Apple" } + ListElement { text: "Coconut" } + ListElement { text: "nucleo_f031k6" } + } + onAccepted: { + focus = false; + } + onActivated: { + focus = false; + } + onFocusChanged: { + if (!focus) { + if (find(editText) === -1) { + editText = textAt(0); + } + } + } + } + CheckBox { + id: runCheckBox + text: 'Run' + enabled: false + ToolTip { + visible: runCheckBox.hovered + delay: 250 + enter: Transition { + NumberAnimation { property: 'opacity'; from: 0.0; to: 1.0 } + } + exit: Transition { + NumberAnimation { property: 'opacity'; from: 1.0; to: 0.0 } + } + 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)) { + 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 + delay: 250 + enter: Transition { + NumberAnimation { property: 'opacity'; from: 0.0; to: 1.0 } + } + exit: Transition { + NumberAnimation { property: 'opacity'; from: 1.0; to: 0.0 } + } + } + } } - CheckBox { - text: 'Build' - enabled: false + Button { + text: 'OK' + onClicked: { + listView.currentItem.item.actionRunning = true; + + listItem.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); + } + row.children[3].clicked(); + } + + if (openEditor.checked) { + if (runCheckBox.checked) { + buttonsModel.setProperty(buttonsModel.count - 1, 'shouldStartEditor', true); + } else { + row.children[1].clicked(); + } + } + + initDialogLoader.sourceComponent = undefined; + content.visible = true; + } } } } @@ -470,25 +584,15 @@ ApplicationWindow { } } - QtLabs.FolderDialog { - id: folderDialog - currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] - onAccepted: { - // popup.open(); - projectsModel.addProject(folder); - - // listView.currentIndex = listView.count; - // swipeView.currentIndex = listView.count; - } - } - Button { - text: 'Add' - onClicked: { - folderDialog.open(); - } + Text { + id: statusBar + padding: 10 + Layout.columnSpan: 2 + // text: 'Status bar' } } + // onClosing: Qt.quit() // onActiveChanged: { // if (active) { diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 081c096..14eb09b 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -397,9 +397,9 @@ def pio_init(self) -> int: error_msg = "PlatformIO project initialization error" if result.returncode == 0: # PlatformIO returns 0 even on some errors (e.g. no '--board' argument) - if 'ERROR' in result.stdout: + if 'error' in result.stdout.lower(): self.logger.error(result.stdout) - raise Exception(error_msg) + raise Exception('\n' + error_msg) self.logger.debug(result.stdout, 'from_subprocess') self.logger.info("successful PlatformIO project initialization") return result.returncode From 0a872fa47fc1da5f3aabb84d4661b92391b07573 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 10 Feb 2020 01:35:06 +0300 Subject: [PATCH 36/54] addition, deletion in list --- stm32pio-gui/app.py | 39 ++++++++++++--- stm32pio-gui/main.qml | 112 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 131 insertions(+), 20 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 3ceb10f..f9c37c7 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -157,7 +157,7 @@ class ProjectListItem(QObject): def __init__(self, project_args: list = None, project_kwargs: dict = None, parent: QObject = None): super().__init__(parent=parent) - self.logThread = QThread() # TODO: can be a 'daemon' type as it runs alongside the main for a long time + self.logThread = QThread() self.handler = HandlerWorker() self.handler.moveToThread(self.logThread) self.handler.addLog.connect(self.logAdded) @@ -206,7 +206,7 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren def init_project(self, *args, **kwargs): try: - # import time + # print('start to init in python') # time.sleep(1) # if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': # raise Exception("Error during initialization") @@ -221,6 +221,7 @@ def init_project(self, *args, **kwargs): pass finally: self.qml_ready.wait() + # print('end to init in python') self.nameChanged.emit() self.stageChanged.emit() self.stateChanged.emit() @@ -254,7 +255,7 @@ def current_stage(self): @Slot() def completed(self): - print('completed from QML') + # print('completed from QML') self.qml_ready.set() self.handler.parent_ready.set() # self.handler.cccompleted() @@ -346,7 +347,15 @@ def __init__(self, projects: list, parent=None): @Slot(int, result=ProjectListItem) def getProject(self, index): - return self.projects[index] + if index >= 0 and index < len(self.projects): + return self.projects[index] + # try: + # print('get index', index) + # p = self.projects[index] + # print('return instance', p) + # return p + # except Exception as e: + # print(e) def rowCount(self, parent=None, *args, **kwargs): return len(self.projects) @@ -379,14 +388,28 @@ def addProject(self, path): @Slot(int) def removeProject(self, index): - self.beginRemoveRows(QModelIndex(), index, index) - self.projects.pop(index) - self.endRemoveRows() + # print('pop index', index) + try: + self.projects[index] + except Exception as e: + print(e) + else: + self.beginRemoveRows(QModelIndex(), index, index) + self.projects.pop(index) + self.endRemoveRows() + # print('removed') def at_exit(self): print('destroy', self) # self.logger.removeHandler(self.handler) + @Slot() + def resetMe(self): + print('resetting...') + self.beginResetModel() + self.projects = [] + self.endResetModel() + def qt_message_handler(mode, context, message): if mode == QtInfoMsg: @@ -414,7 +437,7 @@ def qt_message_handler(mode, context, message): projects = ProjectsList([ ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })), - # ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })), + ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })), # ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) ]) # projects.addProject('Apple') diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 843c86c..0905363 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -25,18 +25,38 @@ ApplicationWindow { } if (initInfo[projectIndex] === 2) { + delete initInfo[projectIndex]; // index can be reused projectsModel.getProject(projectIndex).completed(); - // const indexToOpen = listView.indexToOpenAfterAddition; - // console.log('indexToOpen', indexToOpen); - // if (indexToOpen !== -1) { - // listView.indexToOpenAfterAddition = -1; - // listView.currentIndex = indexToOpen; - // swipeView.currentIndex = indexToOpen; - // } + const indexToOpen = listView.indexToOpenAfterAddition; + console.log('indexToOpen', indexToOpen); + if (indexToOpen !== -1) { + listView.indexToOpenAfterAddition = -1; + listView.currentIndex = indexToOpen; + swipeView.currentIndex = indexToOpen; + } } // Object.keys(initInfo).forEach(key => console.log('index:', key, 'counter:', initInfo[key])); } + // property var indexChangeInfo: ({}) + // function setIndexChangeInfo(projectIndex) { + // if (projectIndex in indexChangeInfo) { + // indexChangeInfo[projectIndex]++; + // } else { + // indexChangeInfo[projectIndex] = 1; + // } + + // if (indexChangeInfo[projectIndex] === 2) { + // delete indexChangeInfo[projectIndex]; // index can be reused + // const indexToRemove = listView.indexToRemoveAfterChangingCurrentIndex; + // if (indexToRemove !== -1) { + // console.log('should remove', indexToRemove, 'based on changing to', projectIndex); + // listView.indexToRemoveAfterChangingCurrentIndex = -1; + // projectsModel.removeProject(indexToRemove); + // } + // } + // } + GridLayout { id: mainGrid columns: 2 @@ -50,7 +70,10 @@ ApplicationWindow { model: projectsModel clip: true property int indexToOpenAfterAddition: -1 - highlight: Rectangle { color: "lightsteelblue"; radius: 5 } + // property int indexToRemoveAfterChangingCurrentIndex: -1 + highlight: Rectangle { color: 'lightsteelblue'; radius: 5 } + highlightMoveDuration: 0 + highlightMoveVelocity: -1 // focus: true delegate: Component { Loader { @@ -104,10 +127,75 @@ ApplicationWindow { projectsModel.addProject(folder); } } - Button { - text: 'Add' - onClicked: { - folderDialog.open(); + Row { + padding: 10 + spacing: 10 + Button { + text: 'Add' + display: AbstractButton.TextBesideIcon + icon.source: 'icons/add.svg' + onClicked: { + folderDialog.open(); + } + } + Button { + id: removeButton + text: 'Remove' + display: AbstractButton.TextBesideIcon + icon.source: 'icons/remove.svg' + onClicked: { + let indexToRemove = listView.currentIndex; + let indexToMove; + if (indexToRemove === (listView.count - 1)) { + if (listView.count === 1) { + indexToMove = -1; + } else { + indexToMove = indexToRemove - 1; + } + } else { + indexToMove = indexToRemove + 1; + } + console.log('indexToMove', indexToMove, 'indexToRemove', indexToRemove); + // listView.indexToRemoveAfterChangingCurrentIndex = indexToRemove; + + // let cnt = 0; + // function bnbn() { + // cnt++; + // if (cnt === 2) { + // function MyTimer() { + // return Qt.createQmlObject("import QtQuick 2.0; Timer {}", removeButton); + // } + + // const t = new MyTimer(); + // t.interval = 1000; + // t.repeat = false; + // t.triggered.connect(function () { + // projectsModel.removeProject(indexToRemove); + // // console.log('after remove', listView.currentIndex); + // // projectsModel.getProject(listView.currentIndex).stateChanged(); + // }) + + // t.start(); + + // const t2 = new MyTimer(); + // t2.interval = 2000; + // t2.repeat = false; + // t2.triggered.connect(function () { + // // projectsModel.removeProject(indexToRemove); + // console.log('after remove', listView.currentIndex); + // projectsModel.getProject(listView.currentIndex).stateChanged(); + // }) + + // t2.start(); + // } + // } + // listView.currentIndexChanged.connect(bnbn); + // swipeView.currentIndexChanged.connect(bnbn); + + listView.currentIndex = indexToMove; + swipeView.currentIndex = indexToMove; + projectsModel.removeProject(indexToRemove); + } } } } From 3ab6e61acfc23ac3b7b8ba045bdd737f717966f8 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 15 Feb 2020 14:16:59 +0300 Subject: [PATCH 37/54] get list of valid PIO boards directly from Python, GUI logging improvements --- TODO.md | 1 + stm32pio-gui/app.py | 142 ++++++++++++++++++++------------------ stm32pio-gui/main.qml | 154 ++++++++++++++++++------------------------ stm32pio/lib.py | 58 ++++++++-------- stm32pio/util.py | 8 +++ 5 files changed, 182 insertions(+), 181 deletions(-) diff --git a/TODO.md b/TODO.md index a86a1fd..e4d3446 100644 --- a/TODO.md +++ b/TODO.md @@ -11,6 +11,7 @@ - [ ] 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 - [ ] 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 - [x] `status` CLI subcommand, why not?.. + - [ ] check for all tools 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 kitchen, e.g. for embedding) - [ ] handle the project folder renaming/movement to other location and/or describe in README diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index f9c37c7..2933829 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -15,11 +15,12 @@ from PySide2.QtCore import QCoreApplication, QUrl, QAbstractItemModel, Property, QAbstractListModel, QModelIndex, \ QObject, Qt, Slot, Signal, QTimer, QThread, qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, \ - QtFatalMsg, QThreadPool, QRunnable + QtFatalMsg, QThreadPool, QRunnable, QStringListModel from PySide2.QtGui import QGuiApplication from PySide2.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine from PySide2.QtQuick import QQuickView + sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) import stm32pio.settings @@ -86,36 +87,47 @@ # # self.parent.logAdded.emit(msg) class LoggingHandler(logging.Handler): - def __init__(self, signal: Signal, parent_ready_event: threading.Event): + def __init__(self, buffer): super().__init__() - self.temp_logs = [] - self.signal = signal - self.parent_ready_event = parent_ready_event + self.buffer = buffer def emit(self, record: logging.LogRecord) -> None: - msg = self.format(record) - # print(msg) - # self.queued_buffer.append(record) - if not self.parent_ready_event.is_set(): - self.temp_logs.append(msg) - else: - if len(self.temp_logs): - self.temp_logs.reverse() - for i in range(len(self.temp_logs)): - m = self.temp_logs.pop() - self.signal.emit(m, record.levelno) - self.signal.emit(msg, record.levelno) + self.buffer.append(record) -class HandlerWorker(QObject): +class LoggingWorker(QObject): addLog = Signal(str, int) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, logger): + super().__init__(parent=None) - self.parent_ready = threading.Event() + self.buffer = collections.deque() + self.stopped = threading.Event() + self.can_flush_log = threading.Event() + self.logging_handler = LoggingHandler(self.buffer) - self.logging_handler = LoggingHandler(self.addLog, self.parent_ready) + 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=special_formatters)) + + self.thread = QThread() + self.moveToThread(self.thread) + + self.thread.started.connect(self.routine) + self.thread.start() + + def routine(self): + while not self.stopped.wait(timeout=0.050): + if self.can_flush_log.is_set(): + try: + record = self.buffer.popleft() + m = self.logging_handler.format(record) + self.addLog.emit(m, record.levelno) + except IndexError: + pass + print('quit logging thread') + self.thread.quit() # self.queued_buffer = collections.deque() @@ -157,19 +169,11 @@ class ProjectListItem(QObject): def __init__(self, project_args: list = None, project_kwargs: dict = None, parent: QObject = None): super().__init__(parent=parent) - self.logThread = QThread() - self.handler = HandlerWorker() - self.handler.moveToThread(self.logThread) - self.handler.addLog.connect(self.logAdded) self.logger = logging.getLogger(f"{stm32pio.lib.__name__}.{id(self)}") - self.logger.addHandler(self.handler.logging_handler) self.logger.setLevel(logging.INFO) - self.handler.logging_handler.setFormatter(stm32pio.util.DispatchingFormatter( - f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", - special=special_formatters)) - - self.logThread.start() + self.logging_worker = LoggingWorker(self.logger) + self.logging_worker.addLog.connect(self.logAdded) self.workers_pool = QThreadPool() self.workers_pool.setMaxThreadCount(1) @@ -228,8 +232,9 @@ def init_project(self, *args, **kwargs): def at_exit(self): print('destroy', self) - self.logger.removeHandler(self.handler) - self.logThread.quit() + self.workers_pool.waitForDone(msecs=-1) + self.logging_worker.stopped.set() + # self.logThread.quit() @Property(str, notify=nameChanged) def name(self): @@ -255,27 +260,30 @@ def current_stage(self): @Slot() def completed(self): - # print('completed from QML') + print('completed from QML') self.qml_ready.set() - self.handler.parent_ready.set() + self.logging_worker.can_flush_log.set() # self.handler.cccompleted() @Slot(str, 'QVariantList') def run(self, action, args): # TODO: queue or smth of jobs - worker = NewProjectActionWorker(self.logger, getattr(self.project, action), args) + worker = NewProjectActionWorker(getattr(self.project, action), args, self.logger) worker.actionResult.connect(self.stateChanged) worker.actionResult.connect(self.stageChanged) worker.actionResult.connect(self.actionResult) self.workers_pool.start(worker) + @Slot() + def test(self): + print('test') class NewProjectActionWorker(QObject, QRunnable): actionResult = Signal(str, bool, arguments=['action', 'success']) - def __init__(self, logger, func, args=None): + def __init__(self, func, args=None, logger=None): QObject.__init__(self, parent=None) QRunnable.__init__(self) @@ -340,9 +348,9 @@ def job(self): class ProjectsList(QAbstractListModel): - def __init__(self, projects: list, parent=None): - super().__init__(parent) - self.projects = projects + def __init__(self, projects: list = None, parent=None): + super().__init__(parent=parent) + self.projects = projects if projects is not None else [] self._finalizer = weakref.finalize(self, self.at_exit) @Slot(int, result=ProjectListItem) @@ -374,17 +382,8 @@ def addProject(self, path): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) project = ProjectListItem(project_args=[path.toLocalFile()], project_kwargs=dict(save_on_destruction=False)) - # project = ProjectListItem() self.projects.append(project) self.endInsertRows() - # project.init_project(path.toLocalFile(), save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' }, logger=project.logger) - - # self.adding_project = None - # def job(): - # self.adding_project = ProjectListItem(path.toLocalFile(), save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' }) - # self.adding_project.moveToThread(app.thread()) - # self.worker = ProjectActionWorker(None, job) - # self.worker.actionResult.connect(self.add) @Slot(int) def removeProject(self, index): @@ -403,12 +402,12 @@ def at_exit(self): print('destroy', self) # self.logger.removeHandler(self.handler) - @Slot() - def resetMe(self): - print('resetting...') - self.beginResetModel() - self.projects = [] - self.endResetModel() + + +def loading(): + # time.sleep(3) + global boards + boards = stm32pio.util.get_platformio_boards() def qt_message_handler(mode, context, message): @@ -435,15 +434,10 @@ def qt_message_handler(mode, context, message): qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') - projects = ProjectsList([ - ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })), - ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })), - # ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False, parameters={ 'board': 'nucleo_f031k6' })) - ]) - # projects.addProject('Apple') - # projects.add(ProjectListItem('../stm32pio-test-project', save_on_destruction=False)) + projects_model = ProjectsList() + boards = [] + boards_model = QStringListModel() - engine.rootContext().setContextProperty('projectsModel', projects) engine.rootContext().setContextProperty('Logging', { 'CRITICAL': logging.CRITICAL, 'ERROR': logging.ERROR, @@ -452,7 +446,25 @@ def qt_message_handler(mode, context, message): 'DEBUG': logging.DEBUG, 'NOTSET': logging.NOTSET }) + engine.rootContext().setContextProperty('projectsModel', projects_model) + engine.rootContext().setContextProperty('boardsModel', boards_model) engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml')) - # engine.quit.connect(app.quit) + + main_window = engine.rootObjects()[0] + + def on_loading(): + boards_model.setStringList(boards) + projects = [ + ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False)), + ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False)), + ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False)) + ] + for p in projects: + projects_model.add(p) + main_window.backendLoaded.emit() + + loader = NewProjectActionWorker(loading) + loader.actionResult.connect(on_loading) + QThreadPool.globalInstance().start(loader) sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 0905363..3003046 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -16,6 +16,9 @@ ApplicationWindow { title: 'stm32pio' color: 'whitesmoke' + signal backendLoaded() + onBackendLoaded: popup.close() + property var initInfo: ({}) function setInitInfo(projectIndex) { if (projectIndex in initInfo) { @@ -27,40 +30,58 @@ ApplicationWindow { if (initInfo[projectIndex] === 2) { delete initInfo[projectIndex]; // index can be reused projectsModel.getProject(projectIndex).completed(); - const indexToOpen = listView.indexToOpenAfterAddition; - console.log('indexToOpen', indexToOpen); - if (indexToOpen !== -1) { - listView.indexToOpenAfterAddition = -1; - listView.currentIndex = indexToOpen; - swipeView.currentIndex = indexToOpen; - } } - // Object.keys(initInfo).forEach(key => console.log('index:', key, 'counter:', initInfo[key])); } - // property var indexChangeInfo: ({}) - // function setIndexChangeInfo(projectIndex) { - // if (projectIndex in indexChangeInfo) { - // indexChangeInfo[projectIndex]++; - // } else { - // indexChangeInfo[projectIndex] = 1; - // } + Popup { + id: popup - // if (indexChangeInfo[projectIndex] === 2) { - // delete indexChangeInfo[projectIndex]; // index can be reused - // const indexToRemove = listView.indexToRemoveAfterChangingCurrentIndex; - // if (indexToRemove !== -1) { - // console.log('should remove', indexToRemove, 'based on changing to', projectIndex); - // listView.indexToRemoveAfterChangingCurrentIndex = -1; - // projectsModel.removeProject(indexToRemove); - // } - // } - // } + visible: true + + parent: Overlay.overlay + anchors.centerIn: parent + modal: true + background: Rectangle { opacity: 0.0 } + closePolicy: Popup.NoAutoClose + + contentItem: Column { + BusyIndicator {} + Text { text: 'Loading...' } + } + } + + QtDialogs.Dialog { + id: settingsDialog + title: 'Settings' + standardButtons: QtDialogs.StandardButton.Save | QtDialogs.StandardButton.Cancel + GridLayout { + columns: 2 + + Label { text: 'Editor' } + TextField {} + + Label { text: 'Verbose output' } + CheckBox {} + } + onAccepted: { + + } + } + + menuBar: MenuBar { + Menu { + title: '&Menu' + Action { text: '&Settings'; onTriggered: settingsDialog.open() } + Action { text: '&About' } + MenuSeparator { } + Action { text: '&Quit'; onTriggered: Qt.quit() } + } + } GridLayout { id: mainGrid columns: 2 - rows: 2 + rows: 1 Column { ListView { @@ -69,8 +90,6 @@ ApplicationWindow { height: 250 model: projectsModel clip: true - property int indexToOpenAfterAddition: -1 - // property int indexToRemoveAfterChangingCurrentIndex: -1 highlight: Rectangle { color: 'lightsteelblue'; radius: 5 } highlightMoveDuration: 0 highlightMoveVelocity: -1 @@ -155,42 +174,6 @@ ApplicationWindow { } else { indexToMove = indexToRemove + 1; } - console.log('indexToMove', indexToMove, 'indexToRemove', indexToRemove); - // listView.indexToRemoveAfterChangingCurrentIndex = indexToRemove; - - // let cnt = 0; - // function bnbn() { - // cnt++; - // if (cnt === 2) { - // function MyTimer() { - // return Qt.createQmlObject("import QtQuick 2.0; Timer {}", removeButton); - // } - - // const t = new MyTimer(); - // t.interval = 1000; - // t.repeat = false; - // t.triggered.connect(function () { - // projectsModel.removeProject(indexToRemove); - // // console.log('after remove', listView.currentIndex); - // // projectsModel.getProject(listView.currentIndex).stateChanged(); - // }) - - // t.start(); - - // const t2 = new MyTimer(); - // t2.interval = 2000; - // t2.repeat = false; - // t2.triggered.connect(function () { - // // projectsModel.removeProject(indexToRemove); - // console.log('after remove', listView.currentIndex); - // projectsModel.getProject(listView.currentIndex).stateChanged(); - // }) - - // t2.start(); - // } - // } - // listView.currentIndexChanged.connect(bnbn); - // swipeView.currentIndexChanged.connect(bnbn); listView.currentIndex = indexToMove; swipeView.currentIndex = indexToMove; @@ -200,11 +183,9 @@ ApplicationWindow { } } - SwipeView { + StackLayout { id: swipeView clip: true - interactive: false - orientation: Qt.Vertical Repeater { model: projectsModel delegate: Component { @@ -242,14 +223,13 @@ ApplicationWindow { } Column { id: content - visible: false + visible: false // StackLayout can be used to show only single widget at a time QtDialogs.MessageDialog { id: projectIncorrectDialog text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + - "The project will be removed from the app. It will not affect any real content" + "The project will be removed from the app. It will not affect any real content" icon: QtDialogs.StandardIcon.Critical onAccepted: { - console.log('on accepted'); const delIndex = swipeView.currentIndex; listView.currentIndex = swipeView.currentIndex + 1; swipeView.currentIndex = swipeView.currentIndex + 1; @@ -257,6 +237,12 @@ ApplicationWindow { buttonGroup.lock = false; } } + Button { + text: 'Test' + onClicked: { + listItem.test(); + } + } ButtonGroup { id: buttonGroup buttons: row.children @@ -264,8 +250,8 @@ ApplicationWindow { signal actionResult(string actionDone, bool success) property bool lock: false onStateReceived: { - if (active && index == swipeView.currentIndex && !lock) { - // console.log('onStateReceived', active, index, !lock); + if (mainWindow.active && (index === swipeView.currentIndex) && !lock) { + // console.log('onStateReceived', mainWindow.active, index, !lock); const state = listItem.state; listItem.stageChanged(); @@ -312,7 +298,7 @@ ApplicationWindow { } Component.onCompleted: { listItem.stateChanged.connect(stateReceived); - swipeView.currentItemChanged.connect(stateReceived); + swipeView.currentIndexChanged.connect(stateReceived); mainWindow.activeChanged.connect(stateReceived); listItem.actionResult.connect(actionResult); @@ -562,13 +548,8 @@ ApplicationWindow { ComboBox { id: board editable: true - model: ListModel { - ListElement { text: "None" } - ListElement { text: "Banana" } - ListElement { text: "Apple" } - ListElement { text: "Coconut" } - ListElement { text: "nucleo_f031k6" } - } + model: boardsModel + textRole: 'display' onAccepted: { focus = false; } @@ -671,15 +652,14 @@ ApplicationWindow { } } } - - Text { - id: statusBar - padding: 10 - Layout.columnSpan: 2 - // text: 'Status bar' - } } + footer: Text { + id: statusBar + // padding: 10 + // Layout.columnSpan: 2 + text: 'Status bar' + } // onClosing: Qt.quit() // onActiveChanged: { diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 14eb09b..7d2694a 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -174,10 +174,10 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction # Given parameter takes precedence over the saved one board = '' if 'board' in parameters and parameters['board'] is not None: - try: - board = self._resolve_board(parameters['board']) - except Exception as e: - self.logger.warning(e) + if parameters['board'] in stm32pio.util.get_platformio_boards(): + board = parameters['board'] + else: + self.logger.warning(f"'{parameters['board']}' was not found in PlatformIO. Run 'platformio boards' for possible names") self.config.set('project', 'board', board) elif self.config.get('project', 'board', fallback=None) is None: self.config.set('project', 'board', board) @@ -307,31 +307,31 @@ def _resolve_project_path(dirty_path: str) -> pathlib.Path: return resolved_path - def _resolve_board(self, board: str) -> str: - """ - Check if given board is a correct board name in the PlatformIO database. Simply get the whole list of all boards - using CLI command and search in the STDOUT - - Args: - board: string representing PlatformIO board name (for example, 'nucleo_f031k6') - - Returns: - same board that has been given if it was found, raise an exception otherwise - """ - - self.logger.debug("searching for PlatformIO board...") - result = subprocess.run([self.config.get('app', 'platformio_cmd'), 'boards'], encoding='utf-8', - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Or, for Python 3.7 and above: - # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) - if result.returncode == 0: - if board not in result.stdout.split(): - raise Exception("wrong PlatformIO STM32 board. Run 'platformio boards' for possible names") - else: - self.logger.debug(f"PlatformIO board {board} was found") - return board - else: - raise Exception("failed to search for PlatformIO boards") + # def _resolve_board(self, board: str) -> str: + # """ + # Check if given board is a correct board name in the PlatformIO database. Simply get the whole list of all boards + # using CLI command and search in the STDOUT + # + # Args: + # board: string representing PlatformIO board name (for example, 'nucleo_f031k6') + # + # Returns: + # same board that has been given if it was found, raise an exception otherwise + # """ + # + # self.logger.debug("searching for PlatformIO board...") + # result = subprocess.run([self.config.get('app', 'platformio_cmd'), 'boards'], encoding='utf-8', + # stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # # Or, for Python 3.7 and above: + # # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) + # if result.returncode == 0: + # if board not in result.stdout.split(): + # raise Exception("wrong PlatformIO STM32 board. Run 'platformio boards' for possible names") + # else: + # self.logger.debug(f"PlatformIO board {board} was found") + # return board + # else: + # raise Exception("failed to search for PlatformIO boards") def generate_code(self) -> int: diff --git a/stm32pio/util.py b/stm32pio/util.py index 2aeecce..51d8cbe 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -2,6 +2,8 @@ import os import threading +from platformio.managers.platform import PlatformManager + module_logger = logging.getLogger(__name__) @@ -34,6 +36,7 @@ def format(self, record): try: return self._formatters['subprocess'].format(record) except AttributeError: + # module_logger.warning pass return super().format(record) @@ -70,3 +73,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): process will be done anyway """ os.close(self.fd_write) + + +def get_platformio_boards(): + pm = PlatformManager() + return [b['id'] for b in pm.get_all_boards() if 'stm32cube' in b['frameworks']] From e555a78d73decef08b0b97b167c2858691facee4 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Mon, 17 Feb 2020 10:54:35 +0300 Subject: [PATCH 38/54] settings support --- stm32pio-gui/app.py | 263 +++++-------- stm32pio-gui/icons/add.svg | 41 ++ stm32pio-gui/icons/remove.svg | 40 ++ stm32pio-gui/main.qml | 652 +------------------------------- stm32pio-gui/main_.qml | 680 ++++++++++++++++++++++++++++++++++ 5 files changed, 857 insertions(+), 819 deletions(-) create mode 100644 stm32pio-gui/icons/add.svg create mode 100644 stm32pio-gui/icons/remove.svg create mode 100644 stm32pio-gui/main_.qml diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 2933829..2dde86c 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -4,21 +4,18 @@ from __future__ import annotations import collections -import functools import logging import pathlib -import queue import sys import threading import time import weakref -from PySide2.QtCore import QCoreApplication, QUrl, QAbstractItemModel, Property, QAbstractListModel, QModelIndex, \ +from PySide2.QtCore import QCoreApplication, QUrl, Property, QAbstractListModel, QModelIndex, \ QObject, Qt, Slot, Signal, QTimer, QThread, qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, \ - QtFatalMsg, QThreadPool, QRunnable, QStringListModel + QtFatalMsg, QThreadPool, QRunnable, QStringListModel, QSettings from PySide2.QtGui import QGuiApplication -from PySide2.QtQml import qmlRegisterType, QQmlEngine, QQmlComponent, QQmlApplicationEngine -from PySide2.QtQuick import QQuickView +from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine sys.path.insert(0, str(pathlib.Path(sys.path[0]).parent)) @@ -32,60 +29,6 @@ special_formatters = {'subprocess': logging.Formatter('%(message)s')} -# class RepetitiveTimer(threading.Thread): -# def __init__(self, stopped, callable, *args, **kwargs): -# super().__init__(*args, **kwargs) -# self.stopped = stopped -# self.callable = callable -# -# def run(self) -> None: -# print('start') -# while not self.stopped.wait(timeout=0.005): -# self.callable() -# print('exitttt') -# -# -# class InternalHandler(logging.Handler): -# def __init__(self, parent: QObject): -# super().__init__() -# self.parent = parent -# # self.temp_logs = [] -# -# self.queued_buffer = collections.deque() -# -# self.stopped = threading.Event() -# self.timer = RepetitiveTimer(self.stopped, self.log) -# self.timer.start() -# -# self._finalizer = weakref.finalize(self, self.at_exit) -# -# def at_exit(self): -# print('exit') -# self.stopped.set() -# -# def log(self): -# if self.parent.is_bound: -# try: -# m = self.format(self.queued_buffer.popleft()) -# # print('initialized', m) -# self.parent.logAdded.emit(m) -# except IndexError: -# pass -# -# def emit(self, record: logging.LogRecord) -> None: -# # msg = self.format(record) -# # print(msg) -# self.queued_buffer.append(record) -# # if not self.parent.is_bound: -# # self.temp_logs.append(msg) -# # else: -# # if len(self.temp_logs): -# # self.temp_logs.reverse() -# # for i in range(len(self.temp_logs)): -# # m = self.temp_logs.pop() -# # self.parent.logAdded.emit(m) -# # self.parent.logAdded.emit(msg) - class LoggingHandler(logging.Handler): def __init__(self, buffer): super().__init__() @@ -129,25 +72,6 @@ def routine(self): print('quit logging thread') self.thread.quit() - # self.queued_buffer = collections.deque() - - # @Slot() - # def cccompleted(self): - # print('completed from ProjectListItem') - # self.parent_ready.set() - - # self.stopped = threading.Event() - # self.timer = RepetitiveTimer(self.stopped, self.log) - # self.timer.start() - # - # def log(self): - # if self.parent_ready: - # try: - # m = self.format(self.queued_buffer.popleft()) - # # print('initialized', m) - # self.addLog.emit(m) - # except IndexError: - # pass class Stm32pio(stm32pio.lib.Stm32pio): @@ -169,9 +93,8 @@ class ProjectListItem(QObject): 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.INFO) + self.logger.setLevel(logging.DEBUG if settings.get('verbose') else logging.INFO) self.logging_worker = LoggingWorker(self.logger) self.logging_worker.addLog.connect(self.logAdded) @@ -179,8 +102,6 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren self.workers_pool.setMaxThreadCount(1) self.workers_pool.setExpiryTimeout(-1) - # self.worker = ProjectActionWorker(self.logger, lambda: None) - self.project = None self._name = 'Loading...' self._state = { 'LOADING': True } @@ -188,7 +109,6 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren self.qml_ready = threading.Event() - # self.destroyed.connect(self.at_exit) self._finalizer2 = weakref.finalize(self, self.at_exit) if project_args is not None: @@ -197,16 +117,7 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren self.init_thread = threading.Thread(target=self.init_project, args=project_args, kwargs=project_kwargs) self.init_thread.start() - # self.init_project(*project_args, **project_kwargs) - # def update_value(): - # # m = 'SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND SEND ' - # # print(m, flush=True) - # self.config.save() - # self.stateChanged.emit() - # # self.logAdded.emit(m) - # self.timer = threading.Timer(5, update_value) - # self.timer.start() def init_project(self, *args, **kwargs): try: @@ -263,7 +174,6 @@ def completed(self): print('completed from QML') self.qml_ready.set() self.logging_worker.can_flush_log.set() - # self.handler.cccompleted() @Slot(str, 'QVariantList') def run(self, action, args): @@ -310,41 +220,6 @@ def run(self): -class ProjectActionWorker(QObject): - actionResult = Signal(str, bool, arguments=['action', 'success']) - - def __init__(self, logger, func, args=None): - super().__init__(parent=None) # QObject with a parent cannot be moved to any thread - - self.logger = logger - self.func = func - if args is None: - self.args = [] - else: - self.args = args - self.name = func.__name__ - - self.thread = QThread() - self.moveToThread(self.thread) - self.actionResult.connect(self.thread.quit) - - self.thread.started.connect(self.job) - self.thread.start() - - - def job(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.actionResult.emit(self.name, success) - class ProjectsList(QAbstractListModel): @@ -355,15 +230,8 @@ def __init__(self, projects: list = None, parent=None): @Slot(int, result=ProjectListItem) def getProject(self, index): - if index >= 0 and index < len(self.projects): + if index in range(len(self.projects)): return self.projects[index] - # try: - # print('get index', index) - # p = self.projects[index] - # print('return instance', p) - # return p - # except Exception as e: - # print(e) def rowCount(self, parent=None, *args, **kwargs): return len(self.projects) @@ -383,6 +251,14 @@ def addProject(self, path): project = ProjectListItem(project_args=[path.toLocalFile()], project_kwargs=dict(save_on_destruction=False)) 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) @@ -395,6 +271,16 @@ def removeProject(self, index): else: self.beginRemoveRows(QModelIndex(), index, index) self.projects.pop(index) + + settings.beginGroup('app') + settings.remove('projects') + settings.beginWriteArray('projects') + for index in range(len(self.projects)): + settings.setArrayIndex(index) + settings.setValue('path', str(self.projects[index].path)) + settings.endArray() + settings.endGroup() + self.endRemoveRows() # print('removed') @@ -407,7 +293,7 @@ def at_exit(self): def loading(): # time.sleep(3) global boards - boards = stm32pio.util.get_platformio_boards() + boards = ['None'] + stm32pio.util.get_platformio_boards() def qt_message_handler(mode, context, message): @@ -424,47 +310,88 @@ def qt_message_handler(mode, context, message): print("%s: %s" % (mode, message)) +DEFAULT_SETTINGS = { + 'editor': '', + 'verbose': False +} + +class Settings(QSettings): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for key, value in 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': + for project in projects_model.projects: + project.logger.setLevel(logging.DEBUG if value else logging.INFO) + + if __name__ == '__main__': if stm32pio.settings.my_os == 'Windows': qInstallMessageHandler(qt_message_handler) app = QGuiApplication(sys.argv) + app.setOrganizationName('ussserrr') + app.setApplicationName('stm32pio') + + # settings = Settings() + # # settings.remove('app/settings') + # 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() qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') + qmlRegisterType(Settings, 'Settings', 1, 0, 'Settings') + # + # projects_model = ProjectsList() + # boards = [] + # boards_model = QStringListModel() + # + # engine.rootContext().setContextProperty('Logging', { + # 'CRITICAL': logging.CRITICAL, + # 'ERROR': logging.ERROR, + # 'WARNING': logging.WARNING, + # 'INFO': logging.INFO, + # 'DEBUG': logging.DEBUG, + # 'NOTSET': logging.NOTSET + # }) + # engine.rootContext().setContextProperty('projectsModel', projects_model) + # engine.rootContext().setContextProperty('boardsModel', boards_model) + # engine.rootContext().setContextProperty('appSettings', settings) - projects_model = ProjectsList() - boards = [] - boards_model = QStringListModel() - - engine.rootContext().setContextProperty('Logging', { - 'CRITICAL': logging.CRITICAL, - 'ERROR': logging.ERROR, - 'WARNING': logging.WARNING, - 'INFO': logging.INFO, - 'DEBUG': logging.DEBUG, - 'NOTSET': logging.NOTSET - }) - engine.rootContext().setContextProperty('projectsModel', projects_model) - engine.rootContext().setContextProperty('boardsModel', boards_model) engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml')) - main_window = engine.rootObjects()[0] - - def on_loading(): - boards_model.setStringList(boards) - projects = [ - ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False)), - ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False)), - ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False)) - ] - for p in projects: - projects_model.add(p) - main_window.backendLoaded.emit() - - loader = NewProjectActionWorker(loading) - loader.actionResult.connect(on_loading) - QThreadPool.globalInstance().start(loader) + # main_window = engine.rootObjects()[0] + # + # def on_loading(): + # boards_model.setStringList(boards) + # projects = [ProjectListItem(project_args=[path], project_kwargs=dict(save_on_destruction=False)) for path in projects_paths] + # # projects = [ + # # ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False)), + # # ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False)), + # # ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False)) + # # ] + # for p in projects: + # projects_model.add(p) + # main_window.backendLoaded.emit() + # + # loader = NewProjectActionWorker(loading) + # loader.actionResult.connect(on_loading) + # QThreadPool.globalInstance().start(loader) sys.exit(app.exec_()) 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/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 index 3003046..11d64b5 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -6,6 +6,7 @@ import QtQuick.Dialogs 1.3 as QtDialogs import Qt.labs.platform 1.1 as QtLabs import ProjectListItem 1.0 +import Settings 1.0 ApplicationWindow { @@ -16,656 +17,5 @@ ApplicationWindow { title: 'stm32pio' color: 'whitesmoke' - signal backendLoaded() - onBackendLoaded: popup.close() - - 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).completed(); - } - } - - Popup { - id: popup - - visible: true - - parent: Overlay.overlay - anchors.centerIn: parent - modal: true - background: Rectangle { opacity: 0.0 } - closePolicy: Popup.NoAutoClose - - contentItem: Column { - BusyIndicator {} - Text { text: 'Loading...' } - } - } - - QtDialogs.Dialog { - id: settingsDialog - title: 'Settings' - standardButtons: QtDialogs.StandardButton.Save | QtDialogs.StandardButton.Cancel - GridLayout { - columns: 2 - - Label { text: 'Editor' } - TextField {} - - Label { text: 'Verbose output' } - CheckBox {} - } - onAccepted: { - - } - } - - menuBar: MenuBar { - Menu { - title: '&Menu' - Action { text: '&Settings'; onTriggered: settingsDialog.open() } - Action { text: '&About' } - MenuSeparator { } - Action { text: '&Quit'; onTriggered: Qt.quit() } - } - } - - GridLayout { - id: mainGrid - columns: 2 - rows: 1 - - Column { - ListView { - id: listView - width: 250 - height: 250 - model: projectsModel - clip: true - highlight: Rectangle { color: 'lightsteelblue'; radius: 5 } - highlightMoveDuration: 0 - highlightMoveVelocity: -1 - // focus: true - delegate: Component { - Loader { - onLoaded: { - setInitInfo(index); - } - sourceComponent: Item { - id: iii - property bool loading: true - property bool actionRunning: false - width: listView.width - height: 40 - property ProjectListItem listItem: projectsModel.getProject(index) - Connections { - target: listItem // sender - onNameChanged: { - loading = false; - } - onActionResult: { - actionRunning = false; - } - } - Row { - Column { - Text { text: 'Name: ' + display.name } - Text { text: 'Stage: ' + display.current_stage } - } - BusyIndicator { - running: iii.loading || iii.actionRunning - width: iii.height - height: iii.height - } - } - MouseArea { - anchors.fill: parent - enabled: !parent.loading - onClicked: { - listView.currentIndex = index; - swipeView.currentIndex = index; - } - } - } - } - } - } - QtLabs.FolderDialog { - id: folderDialog - currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] - onAccepted: { - listView.indexToOpenAfterAddition = listView.count; - projectsModel.addProject(folder); - } - } - Row { - padding: 10 - spacing: 10 - Button { - text: 'Add' - display: AbstractButton.TextBesideIcon - icon.source: 'icons/add.svg' - onClicked: { - folderDialog.open(); - } - } - Button { - id: removeButton - text: 'Remove' - display: AbstractButton.TextBesideIcon - icon.source: 'icons/remove.svg' - onClicked: { - let indexToRemove = listView.currentIndex; - let indexToMove; - if (indexToRemove === (listView.count - 1)) { - if (listView.count === 1) { - indexToMove = -1; - } else { - indexToMove = indexToRemove - 1; - } - } else { - indexToMove = indexToRemove + 1; - } - - listView.currentIndex = indexToMove; - swipeView.currentIndex = indexToMove; - projectsModel.removeProject(indexToRemove); - } - } - } - } - - StackLayout { - id: swipeView - clip: true - Repeater { - model: projectsModel - delegate: Component { - Loader { - // active: SwipeView.isCurrentItem - onLoaded: { - setInitInfo(index); - } - sourceComponent: Column { - property ProjectListItem listItem: projectsModel.getProject(index) - Connections { - target: listItem // sender - onLogAdded: { - if (level === Logging.WARNING) { - log.append('
' + message + '
'); - } else if (level >= Logging.ERROR) { - log.append('
' + message + '
'); - } else { - log.append('
' + message + '
'); - } - } - onNameChanged: { - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = true; - } - - const state = listItem.state; - const s = Object.keys(state).filter(stateName => state[stateName]); - if (s.length === 1 && s[0] === 'EMPTY') { - initDialogLoader.active = true; - } else { - content.visible = true; - } - } - } - Column { - id: content - visible: false // StackLayout can be used to show only single widget at a time - QtDialogs.MessageDialog { - id: projectIncorrectDialog - text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + - "The project will be removed from the app. It will not affect any real content" - icon: QtDialogs.StandardIcon.Critical - onAccepted: { - const delIndex = swipeView.currentIndex; - listView.currentIndex = swipeView.currentIndex + 1; - swipeView.currentIndex = swipeView.currentIndex + 1; - projectsModel.removeProject(delIndex); - buttonGroup.lock = false; - } - } - Button { - text: 'Test' - onClicked: { - listItem.test(); - } - } - ButtonGroup { - id: buttonGroup - buttons: row.children - signal stateReceived() - signal actionResult(string actionDone, bool success) - property bool lock: false - onStateReceived: { - if (mainWindow.active && (index === swipeView.currentIndex) && !lock) { - // console.log('onStateReceived', mainWindow.active, index, !lock); - const state = listItem.state; - listItem.stageChanged(); - - if (state['LOADING']) { - // listView.currentItem.running = true; - } else if (state['INIT_ERROR']) { - // listView.currentItem.running = false; - row.visible = false; - initErrorMessage.visible = true; - } else if (!state['EMPTY']) { - lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) - projectIncorrectDialog.open(); - console.log('no .ioc file'); - } else if (state['EMPTY']) { - // listView.currentItem.running = false; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].palette.button = 'lightgray'; - if (state[buttonsModel.get(i).state]) { - row.children[i].palette.button = 'lightgreen'; - } - } - } - } - } - onActionResult: { - // stopActionButton.visible = false; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = true; - } - } - onClicked: { - // stopActionButton.visible = true; - // listView.currentItem.actionRunning = true; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = false; - row.children[i].glowingVisible = false; - row.children[i].anim.complete(); - // if (buttonsModel.get(i).name === button.text) { - // const b = buttonsModel.get(i); - // const args = b.args ? b.args.split(' ') : []; - // listItem.run(b.action, args); - // } - } - } - Component.onCompleted: { - listItem.stateChanged.connect(stateReceived); - swipeView.currentIndexChanged.connect(stateReceived); - mainWindow.activeChanged.connect(stateReceived); - - listItem.actionResult.connect(actionResult); - } - } - Text { - id: initErrorMessage - visible: false - padding: 10 - text: "The project cannot be initialized" - color: 'red' - } - RowLayout { - id: row - // padding: 10 - // spacing: 10 - z: 1 - Repeater { - model: ListModel { - id: buttonsModel - ListElement { - name: 'Clean' - action: 'clean' - shouldStartEditor: false - } - ListElement { - name: 'Open editor' - action: 'start_editor' - args: 'code' - margin: 15 // margin to visually separate the Clean action as it doesn't represent any state - } - ListElement { - name: 'Initialize' - state: 'INITIALIZED' - action: 'save_config' - shouldRunNext: false - shouldStartEditor: false - } - ListElement { - name: 'Generate' - state: 'GENERATED' - action: 'generate_code' - shouldRunNext: false - shouldStartEditor: false - } - ListElement { - name: 'Initialize PlatformIO' - state: 'PIO_INITIALIZED' - action: 'pio_init' - shouldRunNext: false - shouldStartEditor: false - } - ListElement { - name: 'Patch' - state: 'PATCHED' - action: 'patch' - shouldRunNext: false - shouldStartEditor: false - } - ListElement { - name: 'Build' - state: 'BUILT' - action: 'build' - shouldRunNext: false - shouldStartEditor: false - } - } - delegate: Button { - text: name - enabled: false - property alias glowingVisible: glow.visible - property alias anim: seq - Layout.margins: 10 // insets can be used too - Layout.rightMargin: margin - // rotation: -90 - function runOwnAction() { - listView.currentItem.item.actionRunning = true; - palette.button = 'gold'; - const args = model.args ? model.args.split(' ') : []; - listItem.run(model.action, args); - } - onClicked: { - runOwnAction(); - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - property bool ctrlPressed: false - property bool ctrlPressedLastState: false - property bool shiftPressed: false - property bool shiftPressedLastState: false - // function h() { - // console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button - // } - function shiftHandler() { - // console.log('shiftHandler', shiftPressed, index); - for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... - if (shiftPressed) { - // if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { - row.children[i].palette.button = 'honeydew'; - // } - } else { - buttonGroup.stateReceived(); - // if (Qt.colorEqual(row.children[i].palette.button, 'honeydew')) { - // row.children[i].palette.button = 'lightgray'; - // } - } - } - } - onClicked: { - if (ctrlPressed && model.action !== 'start_editor') { - model.shouldStartEditor = true; - } - if (shiftPressed && index >= 2) { - // run all actions in series - for (let i = 2; i < index; ++i) { - buttonsModel.setProperty(i, 'shouldRunNext', true); - } - row.children[2].clicked(); - return; - } - parent.clicked(); // propagateComposedEvents doesn't work... - } - onPositionChanged: { - if (mouse.modifiers & Qt.ControlModifier) { - ctrlPressed = true; - } else { - ctrlPressed = false; - } - if (ctrlPressedLastState !== ctrlPressed) { - ctrlPressedLastState = ctrlPressed; - } - - if (mouse.modifiers & Qt.ShiftModifier) { - shiftPressed = true; - } else { - shiftPressed = false; - } - if (shiftPressedLastState !== shiftPressed) { - shiftPressedLastState = shiftPressed; - shiftHandler(); - } - } - onEntered: { - statusBar.text = 'Ctrl-click to open the editor specified in the Settings after the operation, Shift-click to perform all actions prior this one (including). Ctrl-Shift-click for both'; - } - onExited: { - statusBar.text = ''; - - ctrlPressed = false; - ctrlPressedLastState = false; - - if (shiftPressed || shiftPressedLastState) { - shiftPressed = false; - shiftPressedLastState = false; - shiftHandler(); - } - } - } - Connections { - target: buttonGroup - onActionResult: { - // console.log('actionDone', actionDone, model.name); - if (actionDone === model.action) { - if (success) { - glow.color = 'lightgreen'; - } else { - palette.button = 'lightcoral'; - glow.color = 'lightcoral'; - } - glow.visible = true; - seq.start(); - - if (model.shouldRunNext) { - model.shouldRunNext = false; - row.children[index + 1].clicked(); // complete task - } - - if (model.shouldStartEditor) { - model.shouldStartEditor = false; - for (let i = 0; i < buttonsModel.count; ++i) { - if (buttonsModel.get(i).action === 'start_editor') { - row.children[i].runOwnAction(); // no additional actions in outer handlers - break; - } - } - } - } - } - } - RectangularGlow { - id: glow - visible: false - anchors.fill: parent - cornerRadius: 25 - glowRadius: 20 - spread: 0.25 - } - SequentialAnimation { - id: seq - loops: 3 - OpacityAnimator { - target: glow - from: 0 - to: 1 - duration: 1000 - } - OpacityAnimator { - target: glow - from: 1 - to: 0 - duration: 1000 - } - } - } - } - } - Rectangle { - width: 800 - height: 380 - ScrollView { - anchors.fill: parent - TextArea { - id: log - // anchors.fill: parent - width: 500 - height: 380 - readOnly: true - selectByMouse: true - wrapMode: Text.WordWrap - font.family: 'Courier' - font.pointSize: 10 - textFormat: TextEdit.RichText - // Component.onCompleted: console.log('textArea completed'); - } - } - } - } - Loader { - id: initDialogLoader - active: false - sourceComponent: Column { - Text { - text: 'To complete initialization you can provide PlatformIO name of the board' - } - Row { - ComboBox { - id: board - editable: true - model: boardsModel - textRole: 'display' - onAccepted: { - focus = false; - } - onActivated: { - focus = false; - } - onFocusChanged: { - if (!focus) { - if (find(editText) === -1) { - editText = textAt(0); - } - } - } - } - CheckBox { - id: runCheckBox - text: 'Run' - enabled: false - ToolTip { - visible: runCheckBox.hovered - delay: 250 - enter: Transition { - NumberAnimation { property: 'opacity'; from: 0.0; to: 1.0 } - } - exit: Transition { - NumberAnimation { property: 'opacity'; from: 1.0; to: 0.0 } - } - 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)) { - 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 - delay: 250 - enter: Transition { - NumberAnimation { property: 'opacity'; from: 0.0; to: 1.0 } - } - exit: Transition { - NumberAnimation { property: 'opacity'; from: 1.0; to: 0.0 } - } - } - } - } - Button { - text: 'OK' - onClicked: { - listView.currentItem.item.actionRunning = true; - - listItem.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); - } - row.children[3].clicked(); - } - - if (openEditor.checked) { - if (runCheckBox.checked) { - buttonsModel.setProperty(buttonsModel.count - 1, 'shouldStartEditor', true); - } else { - row.children[1].clicked(); - } - } - - initDialogLoader.sourceComponent = undefined; - content.visible = true; - } - } - } - } - } - } - } - } - } - } - - footer: Text { - id: statusBar - // padding: 10 - // Layout.columnSpan: 2 - text: 'Status bar' - } - - // onClosing: Qt.quit() - // onActiveChanged: { - // if (active) { - // console.log('window received focus', swipeView.currentIndex); - // } - // } } diff --git a/stm32pio-gui/main_.qml b/stm32pio-gui/main_.qml new file mode 100644 index 0000000..c30e1e8 --- /dev/null +++ b/stm32pio-gui/main_.qml @@ -0,0 +1,680 @@ +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 + width: 1130 + height: 550 + title: 'stm32pio' + color: 'whitesmoke' + + property Settings settings: appSettings + + signal backendLoaded() + onBackendLoaded: popup.close() + + 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).completed(); + } + } + + Popup { + id: popup + + visible: true + + parent: Overlay.overlay + anchors.centerIn: parent + modal: true + background: Rectangle { opacity: 0.0 } + closePolicy: Popup.NoAutoClose + + contentItem: Column { + BusyIndicator {} + Text { text: 'Loading...' } + } + } + + QtDialogs.Dialog { + id: settingsDialog + title: 'Settings' + standardButtons: QtDialogs.StandardButton.Save | QtDialogs.StandardButton.Cancel + GridLayout { + columns: 2 + + Label { text: 'Editor' } + TextField { id: editor; text: settings.get('editor') } + + Label { text: 'Verbose output' } + CheckBox { id: verbose; checked: settings.get('verbose') } + } + onAccepted: { + settings.set('editor', editor.text); + settings.set('verbose', verbose.checked); + } + } + + menuBar: MenuBar { + Menu { + title: '&Menu' + Action { text: '&Settings'; onTriggered: settingsDialog.open() } + Action { text: '&About' } + MenuSeparator { } + Action { text: '&Quit'; onTriggered: Qt.quit() } + } + } + + GridLayout { + id: mainGrid + columns: 2 + rows: 1 + + Column { + ListView { + id: listView + width: 250 + height: 250 + model: projectsModel + clip: true + highlight: Rectangle { + color: 'darkseagreen' + } + highlightMoveDuration: 0 + highlightMoveVelocity: -1 + delegate: Component { + Loader { + onLoaded: { + setInitInfo(index); + } + sourceComponent: Item { + id: iii + property bool loading: true + property bool actionRunning: false + width: listView.width + height: 40 + property ProjectListItem listItem: projectsModel.getProject(index) + Connections { + target: listItem // sender + onNameChanged: { + loading = false; + } + onActionResult: { + actionRunning = false; + } + } + RowLayout { + Column { + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Text { text: '' + display.name + ' ' } + Text { text: display.current_stage } + } + BusyIndicator { + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + running: iii.loading || iii.actionRunning + width: iii.height + height: iii.height + } + } + MouseArea { + anchors.fill: parent + enabled: !parent.loading + onClicked: { + listView.currentIndex = index; + swipeView.currentIndex = index; + } + } + } + } + } + } + QtLabs.FolderDialog { + id: folderDialog + currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] + onAccepted: { + projectsModel.addProject(folder); + } + } + Row { + padding: 10 + spacing: 10 + Button { + text: 'Add' + display: AbstractButton.TextBesideIcon + icon.source: 'icons/add.svg' + onClicked: { + folderDialog.open(); + } + } + Button { + id: removeButton + text: 'Remove' + display: AbstractButton.TextBesideIcon + icon.source: 'icons/remove.svg' + onClicked: { + let indexToRemove = listView.currentIndex; + let indexToMove; + if (indexToRemove === (listView.count - 1)) { + if (listView.count === 1) { + indexToMove = -1; + } else { + indexToMove = indexToRemove - 1; + } + } else { + indexToMove = indexToRemove + 1; + } + + listView.currentIndex = indexToMove; + swipeView.currentIndex = indexToMove; + projectsModel.removeProject(indexToRemove); + } + } + } + } + + StackLayout { + id: swipeView + clip: true + Repeater { + model: projectsModel + delegate: Component { + Loader { + // active: SwipeView.isCurrentItem + onLoaded: { + setInitInfo(index); + } + sourceComponent: Column { + property ProjectListItem listItem: projectsModel.getProject(index) + Connections { + target: listItem // sender + onLogAdded: { + if (level === Logging.WARNING) { + log.append('
' + message + '
'); + } else if (level >= Logging.ERROR) { + log.append('
' + message + '
'); + } else { + log.append('
' + message + '
'); + } + } + onNameChanged: { + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = true; + } + + const state = listItem.state; + const s = Object.keys(state).filter(stateName => state[stateName]); + if (s.length === 1 && s[0] === 'EMPTY') { + initDialogLoader.active = true; + } else { + content.visible = true; + } + } + } + Column { + id: content + visible: false // StackLayout can be used to show only single widget at a time + QtDialogs.MessageDialog { + // TODO: .ioc file can be also removed on init stage (i.e. when initDialog is active) + id: projectIncorrectDialog + text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + + "The project will be removed from the app. It will not affect any real content" + icon: QtDialogs.StandardIcon.Critical + onAccepted: { + const indexToRemove = swipeView.currentIndex; + listView.currentIndex = swipeView.currentIndex + 1; + swipeView.currentIndex = swipeView.currentIndex + 1; + projectsModel.removeProject(indexToRemove); + buttonGroup.lock = false; + } + } + // Button { + // text: 'Test' + // onClicked: { + // listItem.test(); + // } + // } + ButtonGroup { + id: buttonGroup + buttons: row.children + signal stateReceived() + signal actionResult(string actionDone, bool success) + property bool lock: false + onStateReceived: { + if (mainWindow.active && (index === swipeView.currentIndex) && !lock) { + // console.log('onStateReceived', mainWindow.active, index, !lock); + const state = listItem.state; + listItem.stageChanged(); + + if (state['LOADING']) { + // listView.currentItem.running = true; + } else if (state['INIT_ERROR']) { + // listView.currentItem.running = false; + row.visible = false; + initErrorMessage.visible = true; + } else if (!state['EMPTY']) { + lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) + projectIncorrectDialog.open(); + console.log('no .ioc file'); + } else if (state['EMPTY']) { + // listView.currentItem.running = false; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].palette.button = 'lightgray'; + if (state[buttonsModel.get(i).state]) { + row.children[i].palette.button = 'lightgreen'; + } + } + } + } + } + onActionResult: { + // stopActionButton.visible = false; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = true; + } + } + onClicked: { + // stopActionButton.visible = true; + // listView.currentItem.actionRunning = true; + for (let i = 0; i < buttonsModel.count; ++i) { + row.children[i].enabled = false; + row.children[i].glowingVisible = false; + row.children[i].anim.complete(); + // if (buttonsModel.get(i).name === button.text) { + // const b = buttonsModel.get(i); + // const args = b.args ? b.args.split(' ') : []; + // listItem.run(b.action, args); + // } + } + } + Component.onCompleted: { + listItem.stateChanged.connect(stateReceived); + swipeView.currentIndexChanged.connect(stateReceived); + mainWindow.activeChanged.connect(stateReceived); + + listItem.actionResult.connect(actionResult); + } + } + Text { + id: initErrorMessage + visible: false + padding: 10 + text: "The project cannot be initialized" + color: 'red' + } + RowLayout { + id: row + // padding: 10 + // spacing: 10 + z: 1 + Repeater { + model: ListModel { + id: buttonsModel + ListElement { + name: 'Clean' + action: 'clean' + shouldStartEditor: false + } + ListElement { + name: 'Open editor' + action: 'start_editor' + margin: 15 // margin to visually separate the Clean action as it doesn't represent any state + } + ListElement { + name: 'Initialize' + state: 'INITIALIZED' + action: 'save_config' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Generate' + state: 'GENERATED' + action: 'generate_code' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Initialize PlatformIO' + state: 'PIO_INITIALIZED' + action: 'pio_init' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Patch' + state: 'PATCHED' + action: 'patch' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Build' + state: 'BUILT' + action: 'build' + shouldRunNext: false + shouldStartEditor: false + } + } + delegate: Button { + text: name + enabled: false + property alias glowingVisible: glow.visible + property alias anim: seq + Layout.margins: 10 // insets can be used too + Layout.rightMargin: margin + // rotation: -90 + function runOwnAction() { + listView.currentItem.item.actionRunning = true; + palette.button = 'gold'; + let args = []; + if (model.action === 'start_editor') { + args.push(settings.get('editor')); + } + listItem.run(model.action, args); + } + onClicked: { + runOwnAction(); + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + property bool ctrlPressed: false + property bool ctrlPressedLastState: false + property bool shiftPressed: false + property bool shiftPressedLastState: false + // function h() { + // console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button + // } + function shiftHandler() { + // console.log('shiftHandler', shiftPressed, index); + for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... + if (shiftPressed) { + // if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { + row.children[i].palette.button = 'honeydew'; + // } + } else { + buttonGroup.stateReceived(); + // if (Qt.colorEqual(row.children[i].palette.button, 'honeydew')) { + // row.children[i].palette.button = 'lightgray'; + // } + } + } + } + onClicked: { + if (ctrlPressed && model.action !== 'start_editor') { + model.shouldStartEditor = true; + } + if (shiftPressed && index >= 2) { + // run all actions in series + for (let i = 2; i < index; ++i) { + buttonsModel.setProperty(i, 'shouldRunNext', true); + } + row.children[2].clicked(); + return; + } + parent.clicked(); // propagateComposedEvents doesn't work... + } + onPositionChanged: { + if (mouse.modifiers & Qt.ControlModifier) { + ctrlPressed = true; + } else { + ctrlPressed = false; + } + if (ctrlPressedLastState !== ctrlPressed) { + ctrlPressedLastState = ctrlPressed; + } + + if (mouse.modifiers & Qt.ShiftModifier) { + shiftPressed = true; + } else { + shiftPressed = false; + } + if (shiftPressedLastState !== shiftPressed) { + shiftPressedLastState = shiftPressed; + shiftHandler(); + } + } + onEntered: { + statusBar.text = 'Ctrl-click to open the editor specified in the Settings after the operation, Shift-click to perform all actions prior this one (including). Ctrl-Shift-click for both'; + } + onExited: { + statusBar.text = ''; + + ctrlPressed = false; + ctrlPressedLastState = false; + + if (shiftPressed || shiftPressedLastState) { + shiftPressed = false; + shiftPressedLastState = false; + shiftHandler(); + } + } + } + Connections { + target: buttonGroup + onActionResult: { + // console.log('actionDone', actionDone, model.name); + if (actionDone === model.action) { + if (success) { + glow.color = 'lightgreen'; + } else { + palette.button = 'lightcoral'; + glow.color = 'lightcoral'; + } + glow.visible = true; + seq.start(); + + if (model.shouldRunNext) { + model.shouldRunNext = false; + row.children[index + 1].clicked(); // complete task + } + + if (model.shouldStartEditor) { + model.shouldStartEditor = false; + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).action === 'start_editor') { + row.children[i].runOwnAction(); // no additional actions in outer handlers + break; + } + } + } + } + } + } + RectangularGlow { + id: glow + visible: false + anchors.fill: parent + cornerRadius: 25 + glowRadius: 20 + spread: 0.25 + } + SequentialAnimation { + id: seq + loops: 3 + OpacityAnimator { + target: glow + from: 0 + to: 1 + duration: 1000 + } + OpacityAnimator { + target: glow + from: 1 + to: 0 + duration: 1000 + } + } + } + } + } + Rectangle { + width: 800 + height: 380 + ScrollView { + anchors.fill: parent + TextArea { + id: log + // anchors.fill: parent + width: 500 + height: 380 + readOnly: true + selectByMouse: true + wrapMode: Text.WordWrap + font.family: 'Courier' + font.pointSize: 10 + textFormat: TextEdit.RichText + // Component.onCompleted: console.log('textArea completed'); + } + } + } + } + Loader { + id: initDialogLoader + active: false + sourceComponent: Column { + Text { + text: 'To complete initialization you can provide PlatformIO name of the board' + } + Row { + ComboBox { + id: board + editable: true + model: boardsModel + textRole: 'display' + onAccepted: { + focus = false; + } + onActivated: { + focus = false; + } + onFocusChanged: { + if (!focus) { + if (find(editText) === -1) { + editText = textAt(0); + } + } + } + } + CheckBox { + id: runCheckBox + text: 'Run' + enabled: false + ToolTip { + visible: runCheckBox.hovered + delay: 250 + enter: Transition { + NumberAnimation { property: 'opacity'; from: 0.0; to: 1.0 } + } + exit: Transition { + NumberAnimation { property: 'opacity'; from: 1.0; to: 0.0 } + } + 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)) { + 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 + delay: 250 + enter: Transition { + NumberAnimation { property: 'opacity'; from: 0.0; to: 1.0 } + } + exit: Transition { + NumberAnimation { property: 'opacity'; from: 1.0; to: 0.0 } + } + } + } + } + Button { + text: 'OK' + onClicked: { + listView.currentItem.item.actionRunning = true; + + listItem.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); + } + row.children[3].clicked(); + } + + if (openEditor.checked) { + if (runCheckBox.checked) { + buttonsModel.setProperty(buttonsModel.count - 1, 'shouldStartEditor', true); + } else { + row.children[1].clicked(); + } + } + + initDialogLoader.sourceComponent = undefined; + content.visible = true; + } + } + } + } + } + } + } + } + } + } + + footer: Text { + id: statusBar + padding: 10 + // Layout.columnSpan: 2 + text: '' + } + + // onClosing: Qt.quit() + // onActiveChanged: { + // if (active) { + // console.log('window received focus', swipeView.currentIndex); + // } + // } + +} From 3ef4e066e86b209f6cd72e15e8f8d27b7e2c0307 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 19 Feb 2020 10:14:11 +0300 Subject: [PATCH 39/54] basic GUI layout done --- stm32pio-gui/main.qml | 179 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 2 deletions(-) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 11d64b5..0d8107b 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -12,10 +12,185 @@ import Settings 1.0 ApplicationWindow { id: mainWindow visible: true - width: 1130 - height: 550 + minimumWidth: 980 + minimumHeight: 300 + height: 530 title: 'stm32pio' color: 'whitesmoke' + GridLayout { + anchors.fill: parent + rows: 1 + ColumnLayout { + Layout.preferredWidth: 2.5 * parent.width / 12 + Layout.fillHeight: true + + ListView { + id: list + Layout.fillWidth: true + Layout.fillHeight: true + + highlight: Rectangle { color: 'darkseagreen' } + highlightMoveDuration: 0 + highlightMoveVelocity: -1 + currentIndex: 0 + + model: ListModel { + ListElement { + name: '‎⁨MacSSD⁩ ▸ ⁨Пользователи⁩ ▸ ⁨chufyrev⁩ ▸ ⁨Документы⁩ ▸ ⁨STM32⁩ ▸ ⁨stm32cubemx⁩' + state: 'Bla Bla Bla' + busy: false + } + ListElement { + name: 'exec java -jar /opt/stm32cubemx/STM32CubeMX.exe "$@"⁩' + state: 'Abracadabra' + busy: true + } + } + delegate: RowLayout { + ColumnLayout { + Layout.preferredHeight: 50 + Layout.leftMargin: 5 + Layout.rightMargin: 5 + Text { + Layout.alignment: Qt.AlignBottom + Layout.preferredWidth: model.busy ? list.width - parent.height : list.width + elide: Text.ElideRight + maximumLineCount: 1 + text: model.name + } + Text { + Layout.alignment: Qt.AlignTop + Layout.preferredWidth: model.busy ? list.width - parent.height : list.width + elide: Text.ElideRight + maximumLineCount: 1 + text: model.state + } + } + + BusyIndicator { + visible: model.busy + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.preferredWidth: parent.height + Layout.preferredHeight: parent.height + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + Button { + text: 'Add' + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + } + Button { + text: 'Remove' + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + } + } + } + + + StackLayout { + // Screen per project + Layout.preferredWidth: 9.5 * parent.width / 12 + Layout.fillHeight: true + Layout.margins: 10 + + StackLayout { + // Init screen or Work screen + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + id: row + Layout.fillWidth: true + Layout.bottomMargin: 7 + z: 1 + Repeater { + model: ListModel { + id: buttonsModel + ListElement { + name: 'Clean' + action: 'clean' + shouldStartEditor: false + } + ListElement { + name: 'Open editor' + action: 'start_editor' + margin: 15 // margin to visually separate actions as they doesn't represent any state + } + ListElement { + name: 'Initialize' + state: 'INITIALIZED' + action: 'save_config' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Generate' + state: 'GENERATED' + action: 'generate_code' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Init PlatformIO' + state: 'PIO_INITIALIZED' + action: 'pio_init' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Patch' + state: 'PATCHED' + action: 'patch' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Build' + state: 'BUILT' + action: 'build' + shouldRunNext: false + shouldStartEditor: false + } + } + delegate: Button { + text: name + Layout.rightMargin: model.margin + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.rightMargin: 2 + + ScrollView { + anchors.fill: parent + TextArea { + id: log + anchors.fill: parent + readOnly: true + selectByMouse: true + wrapMode: Text.WordWrap + font.family: 'Courier' + font.pointSize: 10 + textFormat: TextEdit.RichText + text: 'AAA BBB' + } + } + } + } + } + } + } } From 785a7e634a55a87f3c6ae9d575ea9a1e5e9f612d Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 22 Feb 2020 23:15:53 +0300 Subject: [PATCH 40/54] refined QML layout, About dialog, format 'cubemx_script_content' --- stm32pio-gui/app.py | 90 ++--- stm32pio-gui/main.qml | 765 +++++++++++++++++++++++++++++++++++------- stm32pio/settings.py | 10 +- 3 files changed, 698 insertions(+), 167 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 2dde86c..f2395b5 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -76,6 +76,7 @@ def routine(self): class Stm32pio(stm32pio.lib.Stm32pio): def save_config(self, parameters: dict = None): + # raise Exception('test') if parameters is not None: for section_name, section_value in parameters.items(): for key, value in section_value.items(): @@ -122,7 +123,7 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren def init_project(self, *args, **kwargs): try: # print('start to init in python') - # time.sleep(1) + # time.sleep(3) # if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': # raise Exception("Error during initialization") self.project = Stm32pio(*args, **kwargs) @@ -277,7 +278,7 @@ def removeProject(self, index): settings.beginWriteArray('projects') for index in range(len(self.projects)): settings.setArrayIndex(index) - settings.setValue('path', str(self.projects[index].path)) + settings.setValue('path', str(self.projects[index].project.path)) settings.endArray() settings.endGroup() @@ -343,55 +344,56 @@ def set(self, key, value): app.setOrganizationName('ussserrr') app.setApplicationName('stm32pio') - # settings = Settings() - # # settings.remove('app/settings') - # 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() + settings = Settings() + # settings.remove('app/settings') + # settings.remove('app/projects') + 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() qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') qmlRegisterType(Settings, 'Settings', 1, 0, 'Settings') - # - # projects_model = ProjectsList() - # boards = [] - # boards_model = QStringListModel() - # - # engine.rootContext().setContextProperty('Logging', { - # 'CRITICAL': logging.CRITICAL, - # 'ERROR': logging.ERROR, - # 'WARNING': logging.WARNING, - # 'INFO': logging.INFO, - # 'DEBUG': logging.DEBUG, - # 'NOTSET': logging.NOTSET - # }) - # engine.rootContext().setContextProperty('projectsModel', projects_model) - # engine.rootContext().setContextProperty('boardsModel', boards_model) - # engine.rootContext().setContextProperty('appSettings', settings) + + projects_model = ProjectsList() + boards = [] + boards_model = QStringListModel() + + engine.rootContext().setContextProperty('Logging', { + 'CRITICAL': logging.CRITICAL, + 'ERROR': logging.ERROR, + 'WARNING': logging.WARNING, + 'INFO': logging.INFO, + 'DEBUG': logging.DEBUG, + '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] - # - # def on_loading(): - # boards_model.setStringList(boards) - # projects = [ProjectListItem(project_args=[path], project_kwargs=dict(save_on_destruction=False)) for path in projects_paths] - # # projects = [ - # # ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False)), - # # ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False)), - # # ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False)) - # # ] - # for p in projects: - # projects_model.add(p) - # main_window.backendLoaded.emit() - # - # loader = NewProjectActionWorker(loading) - # loader.actionResult.connect(on_loading) - # QThreadPool.globalInstance().start(loader) + main_window = engine.rootObjects()[0] + + def on_loading(): + boards_model.setStringList(boards) + projects = [ProjectListItem(project_args=[path], project_kwargs=dict(save_on_destruction=False)) for path in projects_paths] + # projects = [ + # ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False)), + # ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False)), + # ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False)) + # ] + for p in projects: + projects_model.add(p) + main_window.backendLoaded.emit() + + loader = NewProjectActionWorker(loading) + loader.actionResult.connect(on_loading) + QThreadPool.globalInstance().start(loader) sys.exit(app.exec_()) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 0d8107b..9d29d5a 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -3,6 +3,7 @@ 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 @@ -18,174 +19,691 @@ ApplicationWindow { title: 'stm32pio' color: 'whitesmoke' + property Settings settings: appSettings + + signal backendLoaded() + onBackendLoaded: loadingOverlay.close() + + 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).completed(); + } + } + + 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...' } + } + } + + 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 + } + } + } + } + } + + menuBar: MenuBar { + Menu { + title: '&Menu' + Action { text: '&Settings'; onTriggered: settingsDialog.open() } + Action { text: '&About'; onTriggered: aboutDialog.open() } + MenuSeparator { } + Action { text: '&Quit'; onTriggered: Qt.quit() } + } + } + GridLayout { anchors.fill: parent rows: 1 + z: 2 ColumnLayout { - Layout.preferredWidth: 2.5 * parent.width / 12 + Layout.preferredWidth: 2.6 * parent.width / 12 Layout.fillHeight: true ListView { - id: list + id: projectsListView Layout.fillWidth: true Layout.fillHeight: true + clip: true highlight: Rectangle { color: 'darkseagreen' } highlightMoveDuration: 0 highlightMoveVelocity: -1 - currentIndex: 0 - model: ListModel { - ListElement { - name: '‎⁨MacSSD⁩ ▸ ⁨Пользователи⁩ ▸ ⁨chufyrev⁩ ▸ ⁨Документы⁩ ▸ ⁨STM32⁩ ▸ ⁨stm32cubemx⁩' - state: 'Bla Bla Bla' - busy: false - } - ListElement { - name: 'exec java -jar /opt/stm32cubemx/STM32CubeMX.exe "$@"⁩' - state: 'Abracadabra' - busy: true - } - } - delegate: RowLayout { - ColumnLayout { - Layout.preferredHeight: 50 - Layout.leftMargin: 5 - Layout.rightMargin: 5 - Text { - Layout.alignment: Qt.AlignBottom - Layout.preferredWidth: model.busy ? list.width - parent.height : list.width - elide: Text.ElideRight - maximumLineCount: 1 - text: model.name - } - Text { - Layout.alignment: Qt.AlignTop - Layout.preferredWidth: model.busy ? list.width - parent.height : list.width - elide: Text.ElideRight - maximumLineCount: 1 - text: model.state - } - } + model: projectsModel + delegate: Component { + Loader { + onLoaded: setInitInfo(index) + sourceComponent: RowLayout { + id: projectsListItem + property bool loading: true + property bool actionRunning: false + property ProjectListItem project: projectsModel.getProject(index) + Connections { + target: project // sender + onNameChanged: { + loading = false; + } + onActionResult: { + 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.ElideRight + 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.loading || projectsListItem.actionRunning + } - BusyIndicator { - visible: model.busy - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.preferredWidth: parent.height - Layout.preferredHeight: parent.height + MouseArea { + x: parent.x + y: parent.y + width: parent.width + height: parent.height + enabled: !parent.loading + onClicked: { + projectsListView.currentIndex = index; + projectsWorkspaceView.currentIndex = index; + } + } + } } } } + QtLabs.FolderDialog { + id: addProjectFolderDialog + currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] + onAccepted: projectsModel.addProject(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: { + const indexToRemove = projectsListView.currentIndex; + let indexToMoveTo; + if (indexToRemove === (projectsListView.count - 1)) { + if (projectsListView.count === 1) { + indexToMoveTo = -1; + } else { + indexToMoveTo = indexToRemove - 1; + } + } else { + indexToMoveTo = indexToRemove + 1; + } + + projectsListView.currentIndex = indexToMoveTo; + projectsWorkspaceView.currentIndex = indexToMoveTo; + projectsModel.removeProject(indexToRemove); + } } } } + // Screen per project StackLayout { - // Screen per project - Layout.preferredWidth: 9.5 * parent.width / 12 + id: projectsWorkspaceView + Layout.preferredWidth: 9.4 * parent.width / 12 Layout.fillHeight: true - Layout.margins: 10 + Layout.leftMargin: 5 + Layout.rightMargin: 10 + Layout.topMargin: 10 + // clip: true // do not use as it'll clip glow animation - StackLayout { - // Init screen or Work screen - Layout.fillWidth: true - Layout.fillHeight: true + Repeater { + model: projectsModel + delegate: Component { + Loader { + onLoaded: setInitInfo(index) + // Init screen or Work screen + sourceComponent: StackLayout { + currentIndex: -1 + + Layout.fillWidth: true + Layout.fillHeight: true - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - - RowLayout { - id: row - Layout.fillWidth: true - Layout.bottomMargin: 7 - z: 1 - Repeater { - model: ListModel { - id: buttonsModel - ListElement { - name: 'Clean' - action: 'clean' - shouldStartEditor: false + 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 + '
'); + } } - ListElement { - name: 'Open editor' - action: 'start_editor' - margin: 15 // margin to visually separate actions as they doesn't represent any state + onNameChanged: { + for (let i = 0; i < buttonsModel.count; ++i) { + projActionsRow.children[i].enabled = true; + } + + const state = project.state; + const s = Object.keys(state).filter(stateName => state[stateName]); + if (s.length === 1 && s[0] === 'EMPTY') { + initDialogLoader.active = true; + currentIndex = 0; // show init dialog + } else { + currentIndex = 1; // show main view + } } - ListElement { - name: 'Initialize' - state: 'INITIALIZED' - action: 'save_config' - shouldRunNext: false - shouldStartEditor: false + } + + Loader { + id: initDialogLoader + 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 + textRole: 'display' + onAccepted: { + focus = false; + } + onActivated: { + focus = false; + } + onFocusChanged: { + if (!focus) { + if (find(editText) === -1) { + editText = textAt(0); + } + } + } + } + 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)) { + 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 + onClicked: { + 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(); + } + } + + currentIndex = 1; + initDialogLoader.sourceComponent = undefined; + } + } } - ListElement { - name: 'Generate' - state: 'GENERATED' - action: 'generate_code' - shouldRunNext: false - shouldStartEditor: false + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + QtDialogs.MessageDialog { + // TODO: .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.
+ The project will be removed from the app. It will not affect any real content` + icon: QtDialogs.StandardIcon.Critical + onAccepted: { + const indexToRemove = projectsWorkspaceView.currentIndex; + projectsListView.currentIndex = projectsWorkspaceView.currentIndex + 1; + projectsWorkspaceView.currentIndex = projectsWorkspaceView.currentIndex + 1; + projectsModel.removeProject(indexToRemove); + projActionsButtonGroup.lock = false; + } } - ListElement { - name: 'Init PlatformIO' - state: 'PIO_INITIALIZED' - action: 'pio_init' - shouldRunNext: false - shouldStartEditor: false + + Text { + id: initErrorMessage + visible: false + padding: 10 + text: "The project cannot be initialized" + color: 'red' } - ListElement { - name: 'Patch' - state: 'PATCHED' - action: 'patch' - shouldRunNext: false - shouldStartEditor: false + + ButtonGroup { + id: projActionsButtonGroup + buttons: projActionsRow.children + signal stateReceived() + signal actionResult(string actionDone, bool success) + property bool lock: false + onStateReceived: { + if (mainWindow.active && (index === projectsWorkspaceView.currentIndex) && !lock) { + const state = project.state; + project.stageChanged(); + + if (state['LOADING']) { + // + } else if (state['INIT_ERROR']) { + projActionsRow.visible = false; + initErrorMessage.visible = true; + } else if (!state['EMPTY']) { + lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) + projectIncorrectDialog.open(); + } else if (state['EMPTY']) { + for (let i = 0; i < buttonsModel.count; ++i) { + projActionsRow.children[i].palette.button = 'lightgray'; + if (state[buttonsModel.get(i).state]) { + projActionsRow.children[i].palette.button = 'lightgreen'; + } + } + } + } + } + onActionResult: { + for (let i = 0; i < buttonsModel.count; ++i) { + projActionsRow.children[i].enabled = true; + } + } + onClicked: { + for (let i = 0; i < buttonsModel.count; ++i) { + projActionsRow.children[i].enabled = false; + projActionsRow.children[i].glowVisible = false; + } + } + Component.onCompleted: { + project.stateChanged.connect(stateReceived); + projectsWorkspaceView.currentIndexChanged.connect(stateReceived); + mainWindow.activeChanged.connect(stateReceived); + + project.actionResult.connect(actionResult); + } } - ListElement { - name: 'Build' - state: 'BUILT' - action: 'build' - shouldRunNext: false - shouldStartEditor: false + RowLayout { + id: projActionsRow + Layout.fillWidth: true + Layout.bottomMargin: 7 + z: 1 + Repeater { + model: ListModel { + id: buttonsModel + ListElement { + name: 'Clean' + action: 'clean' + shouldStartEditor: false + } + ListElement { + name: 'Open editor' + action: 'start_editor' + margin: 15 // margin to visually separate actions as they doesn't represent any state + } + ListElement { + name: 'Initialize' + state: 'INITIALIZED' + action: 'save_config' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Generate' + state: 'GENERATED' + action: 'generate_code' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Init PlatformIO' + state: 'PIO_INITIALIZED' + action: 'pio_init' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Patch' + state: 'PATCHED' + action: 'patch' + shouldRunNext: false + shouldStartEditor: false + } + ListElement { + name: 'Build' + state: 'BUILT' + action: 'build' + shouldRunNext: false + shouldStartEditor: false + } + } + delegate: Button { + text: name + Layout.rightMargin: model.margin + enabled: false + property alias glowVisible: glow.visible + function runOwnAction() { + projectsListView.currentItem.item.actionRunning = true; + palette.button = 'gold'; + let args = []; + if (model.action === 'start_editor') { + args.push(settings.get('editor')); + } + project.run(model.action, args); + } + onClicked: { + runOwnAction(); + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + property bool ctrlPressed: false + property bool ctrlPressedLastState: false + property bool shiftPressed: false + property bool shiftPressedLastState: false + function shiftHandler() { + for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... + if (shiftPressed) { + projActionsRow.children[i].palette.button = Qt.lighter('lightgreen', 1.2); + } else { + projActionsButtonGroup.stateReceived(); + } + } + } + onClicked: { + if (ctrlPressed && model.action !== 'start_editor') { + model.shouldStartEditor = true; + } + if (shiftPressed && index >= 2) { + // run all actions in series + for (let i = 2; i < index; ++i) { + buttonsModel.setProperty(i, 'shouldRunNext', true); + } + projActionsRow.children[2].clicked(); + return; + } + parent.clicked(); // propagateComposedEvents doesn't work... + } + onPositionChanged: { + if (mouse.modifiers & Qt.ControlModifier) { + ctrlPressed = true; + } else { + ctrlPressed = false; + } + if (ctrlPressedLastState !== ctrlPressed) { + ctrlPressedLastState = ctrlPressed; + } + + if (mouse.modifiers & Qt.ShiftModifier) { + shiftPressed = true; + } else { + shiftPressed = false; + } + if (shiftPressedLastState !== shiftPressed) { + shiftPressedLastState = shiftPressed; + shiftHandler(); + } + } + onEntered: { + statusBar.text = + `Ctrl-click to open the editor specified in the Settings after the operation, + Shift-click to perform all actions prior this one (including). + Ctrl-Shift-click for both`; + } + onExited: { + statusBar.text = ''; + + ctrlPressed = false; + ctrlPressedLastState = false; + + if (shiftPressed || shiftPressedLastState) { + shiftPressed = false; + shiftPressedLastState = false; + shiftHandler(); + } + } + } + Connections { + target: projActionsButtonGroup + onActionResult: { + if (actionDone === model.action) { + if (success) { + glow.color = 'lightgreen'; + } else { + palette.button = 'lightcoral'; + glow.color = 'lightcoral'; + } + glow.visible = true; + + if (model.shouldRunNext) { + model.shouldRunNext = false; + projActionsRow.children[index + 1].clicked(); // complete task + } + + if (model.shouldStartEditor) { + model.shouldStartEditor = false; + for (let i = 0; i < buttonsModel.count; ++i) { + if (buttonsModel.get(i).action === 'start_editor') { + projActionsRow.children[i].runOwnAction(); // no additional actions in outer handlers + break; + } + } + } + } + } + } + RectangularGlow { + id: glow + visible: false + anchors.fill: parent + cornerRadius: 25 + glowRadius: 20 + spread: 0.25 + onVisibleChanged: { + if (visible) { + glowAnimation.start(); + } else { + glowAnimation.complete(); + } + } + SequentialAnimation { + id: glowAnimation + loops: 3 + OpacityAnimator { + target: glow + from: 0 + to: 1 + duration: 1000 + } + OpacityAnimator { + target: glow + from: 1 + to: 0 + duration: 1000 + } + } + } + } + } } - } - delegate: Button { - text: name - Layout.rightMargin: model.margin - } - } - } - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.rightMargin: 2 - - ScrollView { - anchors.fill: parent - TextArea { - id: log - anchors.fill: parent - readOnly: true - selectByMouse: true - wrapMode: Text.WordWrap - font.family: 'Courier' - font.pointSize: 10 - textFormat: TextEdit.RichText - text: 'AAA BBB' + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollView { + anchors.fill: parent + TextArea { + id: log + readOnly: true + selectByMouse: true + wrapMode: Text.WordWrap + font.family: 'Courier' + font.pointSize: 12 + textFormat: TextEdit.RichText + } + } + } } } } @@ -193,4 +711,9 @@ ApplicationWindow { } } } + + footer: Text { + id: statusBar + padding: 10 + } } diff --git a/stm32pio/settings.py b/stm32pio/settings.py index 2de1d6e..a3845ba 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -27,12 +27,18 @@ }, project={ # (default is OK) See CubeMX user manual PDF (UM1718) to get other useful options - 'cubemx_script_content': "config load $cubemx_ioc_full_filename\ngenerate code $project_path\nexit", + 'cubemx_script_content': inspect.cleandoc(''' + config load $cubemx_ioc_full_filename + generate code $project_path + exit + ''') + '\n', # Override the defaults to comply with CubeMX project structure. This should meet INI-style requirements. You # can include existing sections, too (e.g. + # # [env:nucleo_f031k6] - # key=value + # key = value + # # will add a 'key' parameter) 'platformio_ini_patch_content': inspect.cleandoc(''' [platformio] From c9ff29b2f683da4c5b6d5c5f0e9ea8cfccb308fc Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sun, 23 Feb 2020 22:03:26 +0300 Subject: [PATCH 41/54] test on Windows and Linux --- Apple/Apple.ioc | 16 +- Orange/Orange.ioc | 16 +- Peach/Peach.ioc | 16 +- stm32pio-gui/app.py | 2 + stm32pio-gui/main.qml | 2 + stm32pio-gui/main_.qml | 680 ----------------------------------------- 6 files changed, 16 insertions(+), 716 deletions(-) delete mode 100644 stm32pio-gui/main_.qml diff --git a/Apple/Apple.ioc b/Apple/Apple.ioc index 834d9de..a14f0be 100644 --- a/Apple/Apple.ioc +++ b/Apple/Apple.ioc @@ -19,8 +19,8 @@ Mcu.PinsNb=6 Mcu.ThirdPartyNb=0 Mcu.UserConstants= Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.4.0 -MxDb.Version=DB.5.0.40 +MxCube.Version=5.6.0 +MxDb.Version=DB.5.0.60 NVIC.ForceEnableDMAVector=true NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false @@ -47,14 +47,6 @@ PA2.GPIO_Label=VCP_TX PA2.Locked=true PA2.Mode=Asynchronous PA2.Signal=USART1_TX -PCC.Checker=false -PCC.Line=STM32F0x1 -PCC.MCU=STM32F031K6Tx -PCC.PartNumber=STM32F031K6Tx -PCC.Seq0=0 -PCC.Series=STM32F0 -PCC.Temperature=25 -PCC.Vdd=3.6 PF0-OSC_IN.Locked=true PF0-OSC_IN.Mode=HSE-External-Clock-Source PF0-OSC_IN.Signal=RCC_OSC_IN @@ -79,8 +71,8 @@ ProjectManager.MainLocation=Src ProjectManager.NoMain=false ProjectManager.PreviousToolchain= ProjectManager.ProjectBuild=false -ProjectManager.ProjectFileName=stm32pio-test-project.ioc -ProjectManager.ProjectName=stm32pio-test-project +ProjectManager.ProjectFileName=Apple.ioc +ProjectManager.ProjectName=Apple ProjectManager.StackSize=0x400 ProjectManager.TargetToolchain=Other Toolchains (GPDSC) ProjectManager.ToolChainLocation= diff --git a/Orange/Orange.ioc b/Orange/Orange.ioc index 834d9de..60e965e 100644 --- a/Orange/Orange.ioc +++ b/Orange/Orange.ioc @@ -19,8 +19,8 @@ Mcu.PinsNb=6 Mcu.ThirdPartyNb=0 Mcu.UserConstants= Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.4.0 -MxDb.Version=DB.5.0.40 +MxCube.Version=5.6.0 +MxDb.Version=DB.5.0.60 NVIC.ForceEnableDMAVector=true NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false @@ -47,14 +47,6 @@ PA2.GPIO_Label=VCP_TX PA2.Locked=true PA2.Mode=Asynchronous PA2.Signal=USART1_TX -PCC.Checker=false -PCC.Line=STM32F0x1 -PCC.MCU=STM32F031K6Tx -PCC.PartNumber=STM32F031K6Tx -PCC.Seq0=0 -PCC.Series=STM32F0 -PCC.Temperature=25 -PCC.Vdd=3.6 PF0-OSC_IN.Locked=true PF0-OSC_IN.Mode=HSE-External-Clock-Source PF0-OSC_IN.Signal=RCC_OSC_IN @@ -79,8 +71,8 @@ ProjectManager.MainLocation=Src ProjectManager.NoMain=false ProjectManager.PreviousToolchain= ProjectManager.ProjectBuild=false -ProjectManager.ProjectFileName=stm32pio-test-project.ioc -ProjectManager.ProjectName=stm32pio-test-project +ProjectManager.ProjectFileName=Orange.ioc +ProjectManager.ProjectName=Orange ProjectManager.StackSize=0x400 ProjectManager.TargetToolchain=Other Toolchains (GPDSC) ProjectManager.ToolChainLocation= diff --git a/Peach/Peach.ioc b/Peach/Peach.ioc index 834d9de..4e5bdc2 100644 --- a/Peach/Peach.ioc +++ b/Peach/Peach.ioc @@ -19,8 +19,8 @@ Mcu.PinsNb=6 Mcu.ThirdPartyNb=0 Mcu.UserConstants= Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.4.0 -MxDb.Version=DB.5.0.40 +MxCube.Version=5.6.0 +MxDb.Version=DB.5.0.60 NVIC.ForceEnableDMAVector=true NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false @@ -47,14 +47,6 @@ PA2.GPIO_Label=VCP_TX PA2.Locked=true PA2.Mode=Asynchronous PA2.Signal=USART1_TX -PCC.Checker=false -PCC.Line=STM32F0x1 -PCC.MCU=STM32F031K6Tx -PCC.PartNumber=STM32F031K6Tx -PCC.Seq0=0 -PCC.Series=STM32F0 -PCC.Temperature=25 -PCC.Vdd=3.6 PF0-OSC_IN.Locked=true PF0-OSC_IN.Mode=HSE-External-Clock-Source PF0-OSC_IN.Signal=RCC_OSC_IN @@ -79,8 +71,8 @@ ProjectManager.MainLocation=Src ProjectManager.NoMain=false ProjectManager.PreviousToolchain= ProjectManager.ProjectBuild=false -ProjectManager.ProjectFileName=stm32pio-test-project.ioc -ProjectManager.ProjectName=stm32pio-test-project +ProjectManager.ProjectFileName=Peach.ioc +ProjectManager.ProjectName=Peach ProjectManager.StackSize=0x400 ProjectManager.TargetToolchain=Other Toolchains (GPDSC) ProjectManager.ToolChainLocation= diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index f2395b5..73765f8 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -14,6 +14,8 @@ from PySide2.QtCore import QCoreApplication, QUrl, Property, QAbstractListModel, QModelIndex, \ QObject, Qt, Slot, Signal, QTimer, QThread, qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, \ QtFatalMsg, QThreadPool, QRunnable, QStringListModel, QSettings +# for Manjaro +# from PySide2.QtWidgets import QApplication from PySide2.QtGui import QGuiApplication from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 9d29d5a..f1a183e 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -333,6 +333,8 @@ ApplicationWindow { if (find(editText) === -1) { editText = textAt(0); } + } else { + selectAll(); } } } diff --git a/stm32pio-gui/main_.qml b/stm32pio-gui/main_.qml deleted file mode 100644 index c30e1e8..0000000 --- a/stm32pio-gui/main_.qml +++ /dev/null @@ -1,680 +0,0 @@ -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 - width: 1130 - height: 550 - title: 'stm32pio' - color: 'whitesmoke' - - property Settings settings: appSettings - - signal backendLoaded() - onBackendLoaded: popup.close() - - 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).completed(); - } - } - - Popup { - id: popup - - visible: true - - parent: Overlay.overlay - anchors.centerIn: parent - modal: true - background: Rectangle { opacity: 0.0 } - closePolicy: Popup.NoAutoClose - - contentItem: Column { - BusyIndicator {} - Text { text: 'Loading...' } - } - } - - QtDialogs.Dialog { - id: settingsDialog - title: 'Settings' - standardButtons: QtDialogs.StandardButton.Save | QtDialogs.StandardButton.Cancel - GridLayout { - columns: 2 - - Label { text: 'Editor' } - TextField { id: editor; text: settings.get('editor') } - - Label { text: 'Verbose output' } - CheckBox { id: verbose; checked: settings.get('verbose') } - } - onAccepted: { - settings.set('editor', editor.text); - settings.set('verbose', verbose.checked); - } - } - - menuBar: MenuBar { - Menu { - title: '&Menu' - Action { text: '&Settings'; onTriggered: settingsDialog.open() } - Action { text: '&About' } - MenuSeparator { } - Action { text: '&Quit'; onTriggered: Qt.quit() } - } - } - - GridLayout { - id: mainGrid - columns: 2 - rows: 1 - - Column { - ListView { - id: listView - width: 250 - height: 250 - model: projectsModel - clip: true - highlight: Rectangle { - color: 'darkseagreen' - } - highlightMoveDuration: 0 - highlightMoveVelocity: -1 - delegate: Component { - Loader { - onLoaded: { - setInitInfo(index); - } - sourceComponent: Item { - id: iii - property bool loading: true - property bool actionRunning: false - width: listView.width - height: 40 - property ProjectListItem listItem: projectsModel.getProject(index) - Connections { - target: listItem // sender - onNameChanged: { - loading = false; - } - onActionResult: { - actionRunning = false; - } - } - RowLayout { - Column { - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - Text { text: '' + display.name + ' ' } - Text { text: display.current_stage } - } - BusyIndicator { - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - running: iii.loading || iii.actionRunning - width: iii.height - height: iii.height - } - } - MouseArea { - anchors.fill: parent - enabled: !parent.loading - onClicked: { - listView.currentIndex = index; - swipeView.currentIndex = index; - } - } - } - } - } - } - QtLabs.FolderDialog { - id: folderDialog - currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] - onAccepted: { - projectsModel.addProject(folder); - } - } - Row { - padding: 10 - spacing: 10 - Button { - text: 'Add' - display: AbstractButton.TextBesideIcon - icon.source: 'icons/add.svg' - onClicked: { - folderDialog.open(); - } - } - Button { - id: removeButton - text: 'Remove' - display: AbstractButton.TextBesideIcon - icon.source: 'icons/remove.svg' - onClicked: { - let indexToRemove = listView.currentIndex; - let indexToMove; - if (indexToRemove === (listView.count - 1)) { - if (listView.count === 1) { - indexToMove = -1; - } else { - indexToMove = indexToRemove - 1; - } - } else { - indexToMove = indexToRemove + 1; - } - - listView.currentIndex = indexToMove; - swipeView.currentIndex = indexToMove; - projectsModel.removeProject(indexToRemove); - } - } - } - } - - StackLayout { - id: swipeView - clip: true - Repeater { - model: projectsModel - delegate: Component { - Loader { - // active: SwipeView.isCurrentItem - onLoaded: { - setInitInfo(index); - } - sourceComponent: Column { - property ProjectListItem listItem: projectsModel.getProject(index) - Connections { - target: listItem // sender - onLogAdded: { - if (level === Logging.WARNING) { - log.append('
' + message + '
'); - } else if (level >= Logging.ERROR) { - log.append('
' + message + '
'); - } else { - log.append('
' + message + '
'); - } - } - onNameChanged: { - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = true; - } - - const state = listItem.state; - const s = Object.keys(state).filter(stateName => state[stateName]); - if (s.length === 1 && s[0] === 'EMPTY') { - initDialogLoader.active = true; - } else { - content.visible = true; - } - } - } - Column { - id: content - visible: false // StackLayout can be used to show only single widget at a time - QtDialogs.MessageDialog { - // TODO: .ioc file can be also removed on init stage (i.e. when initDialog is active) - id: projectIncorrectDialog - text: "The project was modified outside of the stm32pio and .ioc file is no longer present. " + - "The project will be removed from the app. It will not affect any real content" - icon: QtDialogs.StandardIcon.Critical - onAccepted: { - const indexToRemove = swipeView.currentIndex; - listView.currentIndex = swipeView.currentIndex + 1; - swipeView.currentIndex = swipeView.currentIndex + 1; - projectsModel.removeProject(indexToRemove); - buttonGroup.lock = false; - } - } - // Button { - // text: 'Test' - // onClicked: { - // listItem.test(); - // } - // } - ButtonGroup { - id: buttonGroup - buttons: row.children - signal stateReceived() - signal actionResult(string actionDone, bool success) - property bool lock: false - onStateReceived: { - if (mainWindow.active && (index === swipeView.currentIndex) && !lock) { - // console.log('onStateReceived', mainWindow.active, index, !lock); - const state = listItem.state; - listItem.stageChanged(); - - if (state['LOADING']) { - // listView.currentItem.running = true; - } else if (state['INIT_ERROR']) { - // listView.currentItem.running = false; - row.visible = false; - initErrorMessage.visible = true; - } else if (!state['EMPTY']) { - lock = true; // projectIncorrectDialog.visible is not working correctly (seems like delay or smth.) - projectIncorrectDialog.open(); - console.log('no .ioc file'); - } else if (state['EMPTY']) { - // listView.currentItem.running = false; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].palette.button = 'lightgray'; - if (state[buttonsModel.get(i).state]) { - row.children[i].palette.button = 'lightgreen'; - } - } - } - } - } - onActionResult: { - // stopActionButton.visible = false; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = true; - } - } - onClicked: { - // stopActionButton.visible = true; - // listView.currentItem.actionRunning = true; - for (let i = 0; i < buttonsModel.count; ++i) { - row.children[i].enabled = false; - row.children[i].glowingVisible = false; - row.children[i].anim.complete(); - // if (buttonsModel.get(i).name === button.text) { - // const b = buttonsModel.get(i); - // const args = b.args ? b.args.split(' ') : []; - // listItem.run(b.action, args); - // } - } - } - Component.onCompleted: { - listItem.stateChanged.connect(stateReceived); - swipeView.currentIndexChanged.connect(stateReceived); - mainWindow.activeChanged.connect(stateReceived); - - listItem.actionResult.connect(actionResult); - } - } - Text { - id: initErrorMessage - visible: false - padding: 10 - text: "The project cannot be initialized" - color: 'red' - } - RowLayout { - id: row - // padding: 10 - // spacing: 10 - z: 1 - Repeater { - model: ListModel { - id: buttonsModel - ListElement { - name: 'Clean' - action: 'clean' - shouldStartEditor: false - } - ListElement { - name: 'Open editor' - action: 'start_editor' - margin: 15 // margin to visually separate the Clean action as it doesn't represent any state - } - ListElement { - name: 'Initialize' - state: 'INITIALIZED' - action: 'save_config' - shouldRunNext: false - shouldStartEditor: false - } - ListElement { - name: 'Generate' - state: 'GENERATED' - action: 'generate_code' - shouldRunNext: false - shouldStartEditor: false - } - ListElement { - name: 'Initialize PlatformIO' - state: 'PIO_INITIALIZED' - action: 'pio_init' - shouldRunNext: false - shouldStartEditor: false - } - ListElement { - name: 'Patch' - state: 'PATCHED' - action: 'patch' - shouldRunNext: false - shouldStartEditor: false - } - ListElement { - name: 'Build' - state: 'BUILT' - action: 'build' - shouldRunNext: false - shouldStartEditor: false - } - } - delegate: Button { - text: name - enabled: false - property alias glowingVisible: glow.visible - property alias anim: seq - Layout.margins: 10 // insets can be used too - Layout.rightMargin: margin - // rotation: -90 - function runOwnAction() { - listView.currentItem.item.actionRunning = true; - palette.button = 'gold'; - let args = []; - if (model.action === 'start_editor') { - args.push(settings.get('editor')); - } - listItem.run(model.action, args); - } - onClicked: { - runOwnAction(); - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - property bool ctrlPressed: false - property bool ctrlPressedLastState: false - property bool shiftPressed: false - property bool shiftPressedLastState: false - // function h() { - // console.log('Show "Start the editor after operation" message'); // not for a 'Open editor' button - // } - function shiftHandler() { - // console.log('shiftHandler', shiftPressed, index); - for (let i = 2; i <= index; ++i) { // TODO: magic number, actually... - if (shiftPressed) { - // if (Qt.colorEqual(row.children[i].palette.button, 'lightgray')) { - row.children[i].palette.button = 'honeydew'; - // } - } else { - buttonGroup.stateReceived(); - // if (Qt.colorEqual(row.children[i].palette.button, 'honeydew')) { - // row.children[i].palette.button = 'lightgray'; - // } - } - } - } - onClicked: { - if (ctrlPressed && model.action !== 'start_editor') { - model.shouldStartEditor = true; - } - if (shiftPressed && index >= 2) { - // run all actions in series - for (let i = 2; i < index; ++i) { - buttonsModel.setProperty(i, 'shouldRunNext', true); - } - row.children[2].clicked(); - return; - } - parent.clicked(); // propagateComposedEvents doesn't work... - } - onPositionChanged: { - if (mouse.modifiers & Qt.ControlModifier) { - ctrlPressed = true; - } else { - ctrlPressed = false; - } - if (ctrlPressedLastState !== ctrlPressed) { - ctrlPressedLastState = ctrlPressed; - } - - if (mouse.modifiers & Qt.ShiftModifier) { - shiftPressed = true; - } else { - shiftPressed = false; - } - if (shiftPressedLastState !== shiftPressed) { - shiftPressedLastState = shiftPressed; - shiftHandler(); - } - } - onEntered: { - statusBar.text = 'Ctrl-click to open the editor specified in the Settings after the operation, Shift-click to perform all actions prior this one (including). Ctrl-Shift-click for both'; - } - onExited: { - statusBar.text = ''; - - ctrlPressed = false; - ctrlPressedLastState = false; - - if (shiftPressed || shiftPressedLastState) { - shiftPressed = false; - shiftPressedLastState = false; - shiftHandler(); - } - } - } - Connections { - target: buttonGroup - onActionResult: { - // console.log('actionDone', actionDone, model.name); - if (actionDone === model.action) { - if (success) { - glow.color = 'lightgreen'; - } else { - palette.button = 'lightcoral'; - glow.color = 'lightcoral'; - } - glow.visible = true; - seq.start(); - - if (model.shouldRunNext) { - model.shouldRunNext = false; - row.children[index + 1].clicked(); // complete task - } - - if (model.shouldStartEditor) { - model.shouldStartEditor = false; - for (let i = 0; i < buttonsModel.count; ++i) { - if (buttonsModel.get(i).action === 'start_editor') { - row.children[i].runOwnAction(); // no additional actions in outer handlers - break; - } - } - } - } - } - } - RectangularGlow { - id: glow - visible: false - anchors.fill: parent - cornerRadius: 25 - glowRadius: 20 - spread: 0.25 - } - SequentialAnimation { - id: seq - loops: 3 - OpacityAnimator { - target: glow - from: 0 - to: 1 - duration: 1000 - } - OpacityAnimator { - target: glow - from: 1 - to: 0 - duration: 1000 - } - } - } - } - } - Rectangle { - width: 800 - height: 380 - ScrollView { - anchors.fill: parent - TextArea { - id: log - // anchors.fill: parent - width: 500 - height: 380 - readOnly: true - selectByMouse: true - wrapMode: Text.WordWrap - font.family: 'Courier' - font.pointSize: 10 - textFormat: TextEdit.RichText - // Component.onCompleted: console.log('textArea completed'); - } - } - } - } - Loader { - id: initDialogLoader - active: false - sourceComponent: Column { - Text { - text: 'To complete initialization you can provide PlatformIO name of the board' - } - Row { - ComboBox { - id: board - editable: true - model: boardsModel - textRole: 'display' - onAccepted: { - focus = false; - } - onActivated: { - focus = false; - } - onFocusChanged: { - if (!focus) { - if (find(editText) === -1) { - editText = textAt(0); - } - } - } - } - CheckBox { - id: runCheckBox - text: 'Run' - enabled: false - ToolTip { - visible: runCheckBox.hovered - delay: 250 - enter: Transition { - NumberAnimation { property: 'opacity'; from: 0.0; to: 1.0 } - } - exit: Transition { - NumberAnimation { property: 'opacity'; from: 1.0; to: 0.0 } - } - 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)) { - 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 - delay: 250 - enter: Transition { - NumberAnimation { property: 'opacity'; from: 0.0; to: 1.0 } - } - exit: Transition { - NumberAnimation { property: 'opacity'; from: 1.0; to: 0.0 } - } - } - } - } - Button { - text: 'OK' - onClicked: { - listView.currentItem.item.actionRunning = true; - - listItem.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); - } - row.children[3].clicked(); - } - - if (openEditor.checked) { - if (runCheckBox.checked) { - buttonsModel.setProperty(buttonsModel.count - 1, 'shouldStartEditor', true); - } else { - row.children[1].clicked(); - } - } - - initDialogLoader.sourceComponent = undefined; - content.visible = true; - } - } - } - } - } - } - } - } - } - } - - footer: Text { - id: statusBar - padding: 10 - // Layout.columnSpan: 2 - text: '' - } - - // onClosing: Qt.quit() - // onActiveChanged: { - // if (active) { - // console.log('window received focus', swipeView.currentIndex); - // } - // } - -} From 71bea7d16b6a67f74c89979ca60d8ee3220c8260 Mon Sep 17 00:00:00 2001 From: usserr Date: Mon, 24 Feb 2020 01:56:07 +0300 Subject: [PATCH 42/54] docs for the 'util' module and for the new features in the 'lib', some improvements --- TODO.md | 3 +- stm32pio-gui/app.py | 4 +- stm32pio-gui/main.qml | 2 +- stm32pio/app.py | 5 +- stm32pio/lib.py | 155 ++++++++++++++++++++++-------------------- stm32pio/util.py | 94 ++++++++++++++++++------- 6 files changed, 159 insertions(+), 104 deletions(-) diff --git a/TODO.md b/TODO.md index e4d3446..4158645 100644 --- a/TODO.md +++ b/TODO.md @@ -26,4 +26,5 @@ - [ ] 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 + - [ ] `__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` + - [ ] parse `platformio.ini` to check its correctness in state getter diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 73765f8..91a84ea 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -28,8 +28,6 @@ -special_formatters = {'subprocess': logging.Formatter('%(message)s')} - class LoggingHandler(logging.Handler): def __init__(self, buffer): @@ -54,7 +52,7 @@ def __init__(self, logger): 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=special_formatters)) + special=stm32pio.util.special_formatters)) self.thread = QThread() self.moveToThread(self.thread) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index f1a183e..997f94f 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -172,7 +172,7 @@ ApplicationWindow { Layout.preferredWidth: busy.running ? (projectsListView.width - parent.height - leftPadding) : projectsListView.width - elide: Text.ElideRight + elide: Text.ElideMiddle maximumLineCount: 1 text: `${display.name}` } diff --git a/stm32pio/app.py b/stm32pio/app.py index e803e22..aaf3011 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -86,18 +86,17 @@ def main(sys_argv=None) -> int: logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance handler = logging.StreamHandler() logger.addHandler(handler) - special_formatters = {'subprocess': logging.Formatter('%(message)s')} # Currently only 2 levels of verbosity through the '-v' option are counted (INFO (default) and DEBUG (-v)) if args is not None and args.subcommand is not None and args.verbose: logger.setLevel(logging.DEBUG) handler.setFormatter(stm32pio.util.DispatchingFormatter( f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", - special=special_formatters)) + special=stm32pio.util.special_formatters)) logger.debug("debug logging enabled") elif args is not None and args.subcommand is not None: logger.setLevel(logging.INFO) handler.setFormatter(stm32pio.util.DispatchingFormatter("%(levelname)-8s %(message)s", - special=special_formatters)) + special=stm32pio.util.special_formatters)) else: logger.setLevel(logging.INFO) handler.setFormatter(logging.Formatter("%(message)s")) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 7d2694a..333e37c 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -27,12 +27,13 @@ class ProjectStage(enum.IntEnum): Hint: Files/folders to be present on every project state: UNDEFINED: use this state to indicate none of the states below. Also, when we do not have any .ioc file the - Stm32pio class cannot be instantiated (constructor raises an exception) + Stm32pio class instance cannot be created (constructor raises an exception) + EMPTY: ['project.ioc'] INITIALIZED: ['project.ioc', 'stm32pio.ini'] GENERATED: ['Inc', 'Src', 'project.ioc', 'stm32pio.ini'] PIO_INITIALIZED (on case-sensitive FS): ['test', 'include', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'lib', 'project.ioc', '.travis.yml', 'src'] - PIO_INI_PATCHED: ['test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'lib', 'project.ioc', '.travis.yml'] + PATCHED: ['test', 'Inc', 'platformio.ini', '.gitignore', 'Src', 'lib', 'project.ioc', '.travis.yml'] BUILT: same as above + '.pio' folder with build artifacts (such as .pio/build/nucleo_f031k6/firmware.bin, .pio/build/nucleo_f031k6/firmware.elf) """ @@ -59,38 +60,61 @@ def __str__(self): class ProjectState(collections.OrderedDict): """ - is not protected from incorrect usage (no checks) + The ordered dictionary subclass suitable for storing the Stm32pio instances state. For example: + { + ProjectStage.UNDEFINED: True, # doesn't necessarily means that the project is messed up, see below + ProjectStage.EMPTY: True, + ProjectStage.INITIALIZED: True, + ProjectStage.GENERATED: False, + ProjectStage.PIO_INITIALIZED: False, + ProjectStage.PATCHED: False, + ProjectStage.BUILT: False, + } + It is also extended with additional properties providing useful information such as obtaining the project current + stage. + + The class has no special constructor so its filling - both stages and their order - is a responsibility of the + external code. It also has no protection nor checks for its internal correctness. Anyway, it is intended to be used + (i.e. creating) only by the internal code of this library so there should not be any worries. """ def __str__(self): - return '\n'.join(f"{'✅ ' if state_value else '❌ '} {str(state_name)}" - for state_name, state_value in self.items() if state_name != ProjectStage.UNDEFINED) + """ + Pretty human-readable complete representation of the project state (not including the service one UNDEFINED to + not confuse the end-user) + """ + return '\n'.join(f"{'✅ ' if stage_value else '❌ '} {str(stage_name)}" + for stage_name, stage_value in self.items() if stage_name != ProjectStage.UNDEFINED) @property - def current_stage(self): - last_consistent_state = ProjectStage.UNDEFINED - zero_found = False + def current_stage(self) -> ProjectStage: + last_consistent_stage = ProjectStage.UNDEFINED + break_found = False - # Search for a consecutive sequence of 1's and find the last of them. For example, if the array is - # [1,1,1,0,1,0,0] + # Search for a consecutive sequence of True's and find the last of them. For example, if the array is + # [1,1,1,0,0,0,0] # ^ # we should consider 2 as the last index for name, value in self.items(): if value: - if zero_found: - # Fall back to the UNDEFINED stage if we have breaks in conditions results array. For example, in [1,1,1,0,1,0,0] - # we still return UNDEFINED as it doesn't look like a correct combination of files actually - last_consistent_state = ProjectStage.UNDEFINED + if break_found: + # Fall back to the UNDEFINED stage if we have breaks in conditions results array. E.g., for + # [1,1,1,0,1,0,0] + # we should return UNDEFINED as it doesn't look like a correct set of files actually + last_consistent_stage = ProjectStage.UNDEFINED break else: - last_consistent_state = name + last_consistent_stage = name else: - zero_found = True + break_found = True - return last_consistent_state + return last_consistent_stage @property - def is_consistent(self): + def is_consistent(self) -> bool: + """ + Whether the state has been went through the stages consequentially or not (the method is currently unused) + """ return self.current_stage != ProjectStage.UNDEFINED @@ -118,7 +142,8 @@ def save(self) -> int: self.project.logger.debug("stm32pio.ini config file has been saved") return 0 except Exception as e: - self.project.logger.warning(f"cannot save the config: {e}", exc_info=self.project.logger.isEnabledFor(logging.DEBUG)) + self.project.logger.warning(f"cannot save the config: {e}", + exc_info=self.project.logger.isEnabledFor(logging.DEBUG)) return -1 @@ -143,12 +168,18 @@ class Stm32pio: dirty_path (str): path to the project parameters (dict): additional parameters to set on initialization stage save_on_destruction (bool): register or not the finalizer that saves the config to file + logger (logging.Logger): if an external logger is given, it will be used, otherwise the new one will be created + (unique for every instance) """ - def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, logger: logging.Logger = None): + def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction: bool = True, + logger: logging.Logger = None): + if parameters is None: parameters = {} + # The individual loggers for every single project allow to fine-tune the output when multiple projects are + # created by the third-party code. if logger is not None: self.logger = logger else: @@ -177,7 +208,8 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction if parameters['board'] in stm32pio.util.get_platformio_boards(): board = parameters['board'] else: - self.logger.warning(f"'{parameters['board']}' was not found in PlatformIO. Run 'platformio boards' for possible names") + self.logger.warning(f"'{parameters['board']}' was not found in PlatformIO. " + "Run 'platformio boards' for possible names") self.config.set('project', 'board', board) elif self.config.get('project', 'board', fallback=None) is None: self.config.set('project', 'board', board) @@ -191,37 +223,39 @@ def __repr__(self): @property - def state(self): + def state(self) -> ProjectState: """ + Constructing and returning the current state of the project (tweaked dict, see ProjectState docs) """ # self.logger.debug(f"project content: {[item.name for item in self.path.iterdir()]}") try: platformio_ini_is_patched = self.platformio_ini_is_patched() - except: + except (FileNotFoundError, ValueError): platformio_ini_is_patched = False - states_conditions = collections.OrderedDict() - # Fill the ordered dictionary with the conditions results - states_conditions[ProjectStage.UNDEFINED] = [True] - states_conditions[ProjectStage.EMPTY] = [self.ioc_file.is_file()] - states_conditions[ProjectStage.INITIALIZED] = [self.path.joinpath(stm32pio.settings.config_file_name).is_file()] - states_conditions[ProjectStage.GENERATED] = [self.path.joinpath('Inc').is_dir() and + # Create the temporary ordered dictionary and fill it with the conditions results arrays + stages_conditions = collections.OrderedDict() + stages_conditions[ProjectStage.UNDEFINED] = [True] + stages_conditions[ProjectStage.EMPTY] = [self.ioc_file.is_file()] + stages_conditions[ProjectStage.INITIALIZED] = [self.path.joinpath(stm32pio.settings.config_file_name).is_file()] + stages_conditions[ProjectStage.GENERATED] = [self.path.joinpath('Inc').is_dir() and len(list(self.path.joinpath('Inc').iterdir())) > 0, self.path.joinpath('Src').is_dir() and len(list(self.path.joinpath('Src').iterdir())) > 0] - states_conditions[ProjectStage.PIO_INITIALIZED] = [ + stages_conditions[ProjectStage.PIO_INITIALIZED] = [ self.path.joinpath('platformio.ini').is_file() and self.path.joinpath('platformio.ini').stat().st_size > 0] - states_conditions[ProjectStage.PATCHED] = [ + stages_conditions[ProjectStage.PATCHED] = [ platformio_ini_is_patched, not self.path.joinpath('include').is_dir()] - states_conditions[ProjectStage.BUILT] = [ + stages_conditions[ProjectStage.BUILT] = [ self.path.joinpath('.pio').is_dir() and any([item.is_file() for item in self.path.joinpath('.pio').rglob('*firmware*')])] + # Fold arrays and save results in ProjectState instance conditions_results = ProjectState() - for state, conditions in states_conditions.items(): + for state, conditions in stages_conditions.items(): conditions_results[state] = all(condition is True for condition in conditions) return conditions_results @@ -229,7 +263,7 @@ def state(self): def _find_ioc_file(self) -> pathlib.Path: """ - Find and return an .ioc file. If there are more than one, return first. If no .ioc file is present raise + Find and return an .ioc file. If there are more than one return first. If no .ioc file is present raise FileNotFoundError exception Returns: @@ -242,7 +276,7 @@ def _find_ioc_file(self) -> pathlib.Path: if ioc_file: ioc_file = pathlib.Path(ioc_file).expanduser().resolve() self.logger.debug(f"use {ioc_file.name} file from the INI config") - if (not ioc_file.is_file()): + if not ioc_file.is_file(): raise FileNotFoundError(error_message) return ioc_file else: @@ -292,7 +326,7 @@ def _load_config_file(self) -> Config: @staticmethod def _resolve_project_path(dirty_path: str) -> pathlib.Path: """ - Handle 'path/to/proj' and 'path/to/proj/', '.' (current directory) and other cases + Handle 'path/to/proj', 'path/to/proj/', '.', '../proj' and other cases Args: dirty_path (str): some directory in the filesystem @@ -307,43 +341,17 @@ def _resolve_project_path(dirty_path: str) -> pathlib.Path: return resolved_path - # def _resolve_board(self, board: str) -> str: - # """ - # Check if given board is a correct board name in the PlatformIO database. Simply get the whole list of all boards - # using CLI command and search in the STDOUT - # - # Args: - # board: string representing PlatformIO board name (for example, 'nucleo_f031k6') - # - # Returns: - # same board that has been given if it was found, raise an exception otherwise - # """ - # - # self.logger.debug("searching for PlatformIO board...") - # result = subprocess.run([self.config.get('app', 'platformio_cmd'), 'boards'], encoding='utf-8', - # stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # # Or, for Python 3.7 and above: - # # result = subprocess.run(['platformio', 'boards'], encoding='utf-8', capture_output=True) - # if result.returncode == 0: - # if board not in result.stdout.split(): - # raise Exception("wrong PlatformIO STM32 board. Run 'platformio boards' for possible names") - # else: - # self.logger.debug(f"PlatformIO board {board} was found") - # return board - # else: - # raise Exception("failed to search for PlatformIO boards") - - def generate_code(self) -> int: """ - Call STM32CubeMX app as a 'java -jar' file to generate the code from the .ioc file. Pass commands to the + Call STM32CubeMX app as 'java -jar' file to generate the code from the .ioc file. Pass commands to the STM32CubeMX in a temp file Returns: return code on success, raises an exception otherwise """ - # Use mkstemp() instead of higher-level API for compatibility with Windows (see tempfile docs for more details) + # Use mkstemp() instead of the higher-level API for the compatibility with the Windows (see tempfile docs for + # more details) cubemx_script_file, cubemx_script_name = tempfile.mkstemp() # We should necessarily remove the temp directory, so do not let any exception break our plans @@ -355,14 +363,16 @@ def generate_code(self) -> int: self.logger.info("starting to generate a code from the CubeMX .ioc file...") command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q', - cubemx_script_name, '-s'] # -q: read commands from file, -s: silent performance + cubemx_script_name, '-s'] # -q: read the commands from the file, -s: silent performance with stm32pio.util.LogPipe(self.logger, logging.DEBUG) as log_pipe: result = subprocess.run(command_arr, stdout=log_pipe, stderr=log_pipe) except Exception as e: - raise e # re-raise an exception after the final block + raise e # re-raise an exception after the 'finally' block finally: pathlib.Path(cubemx_script_name).unlink() + # TODO: doesn't necessarily means the correct generation (e.g. migration dialog has appeared and 'Cancel' was + # chosen), probably should analyze the output if result.returncode == 0: self.logger.info("successful code generation") return result.returncode @@ -375,7 +385,7 @@ def generate_code(self) -> int: def pio_init(self) -> int: """ Call PlatformIO CLI to initialize a new project. It uses parameters (path, board) collected before so the - confirmation of the data presence is on a user + confirmation of the data presence is lying on the invoking code Returns: return code of the PlatformIO on success, raises an exception otherwise @@ -411,7 +421,7 @@ def pio_init(self) -> int: def platformio_ini_is_patched(self) -> bool: """ Check whether 'platformio.ini' config file is patched or not. It doesn't check for complete project patching - (e.g. unnecessary folders deletion). Throws an error on non-existing file and on incorrect patch or file + (e.g. unnecessary folders deletion). Throws errors on non-existing file and on incorrect patch or file Returns: boolean indicating a result @@ -424,13 +434,14 @@ def platformio_ini_is_patched(self) -> bool: except FileNotFoundError as e: raise e except Exception as e: - raise Exception("'platformio.ini' file is incorrect") from e + # Re-raise parsing exceptions as ValueError + raise ValueError("'platformio.ini' file is incorrect") from e patch_config = configparser.ConfigParser(interpolation=None) try: patch_config.read_string(self.config.get('project', 'platformio_ini_patch_content')) except Exception as e: - raise Exception("Desired patch content is invalid (should satisfy INI-format requirements)") from e + raise ValueError("Desired patch content is invalid (should satisfy INI-format requirements)") from e for patch_section in patch_config.sections(): if platformio_ini.has_section(patch_section): @@ -449,7 +460,7 @@ def platformio_ini_is_patched(self) -> bool: def patch(self) -> None: """ Patch platformio.ini file by a user's patch. By default, it sets the created earlier (by CubeMX 'Src' and 'Inc') - folders as sources. configparser doesn't preserve any comments unfortunately so keep in mid that all of them + folders as sources. configparser doesn't preserve any comments unfortunately so keep in mind that all of them will be lost at this stage. Also, the order may be violated. In the end, remove old empty folders """ diff --git a/stm32pio/util.py b/stm32pio/util.py index 51d8cbe..9ca1b37 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -1,3 +1,7 @@ +""" +Some auxiliary entities not falling into other categories +""" + import logging import os import threading @@ -7,74 +11,116 @@ module_logger = logging.getLogger(__name__) -old_log_record_factory = logging.getLogRecordFactory() + + +# Do not add or remove any information from the message and simply pass it 'as-is' +special_formatters = { 'subprocess': logging.Formatter('%(message)s') } + +default_log_record_factory = logging.getLogRecordFactory() + def log_record_factory(*log_record_args, **log_record_kwargs): - args_idx = 5 + """ + Replace the default factory of logging.LogRecord's instances so we can handle our special logging flags + """ + args_idx = 5 # index of 'args' argument in the positional arguments list + if 'from_subprocess' in log_record_args[args_idx]: - new_log_record_args = log_record_args[:args_idx] +\ - (tuple(arg for arg in log_record_args[args_idx] if arg != 'from_subprocess'),) +\ + # Remove our custom flag from the tuple (it is inside a tuple that is inside a list) + new_log_record_args = log_record_args[:args_idx] + \ + (tuple(arg for arg in log_record_args[args_idx] if arg != 'from_subprocess'),) + \ log_record_args[args_idx + 1:] - record = old_log_record_factory(*new_log_record_args, **log_record_kwargs) + # Construct an ordinary LogRecord and append our flag as an attribute + record = default_log_record_factory(*new_log_record_args, **log_record_kwargs) record.from_subprocess = True else: - record = old_log_record_factory(*log_record_args, **log_record_kwargs) + record = default_log_record_factory(*log_record_args, **log_record_kwargs) + return record + logging.setLogRecordFactory(log_record_factory) + class DispatchingFormatter(logging.Formatter): + """ + The wrapper around the ordinary logging.Formatter allowing to have multiple formatters for different purposes. + 'extra' argument of the log() function has a similar intention but different mechanics + """ + def __init__(self, *args, special: dict = None, **kwargs): - if isinstance(special, dict) and all(isinstance(formatter, logging.Formatter) for formatter in special.values()): + super().__init__(*args, **kwargs) + + # Store all provided formatters in an internal variable + if isinstance(special, dict) and all(isinstance(value, logging.Formatter) for value in special.values()): self._formatters = special else: module_logger.warning(f"'special' argument is for providing custom formatters for special logging events " "and should be a dictionary with logging.Formatter values") self._formatters = {} - super().__init__(*args, **kwargs) - def format(self, record): + self.warn_was_shown = False + + def format(self, record: logging.LogRecord) -> str: + """ + Use suitable formatter based on the LogRecord attributes + """ if hasattr(record, 'from_subprocess') and record.from_subprocess: - try: + if 'subprocess' in self._formatters: return self._formatters['subprocess'].format(record) - except AttributeError: - # module_logger.warning - pass + elif not self.warn_was_shown: + module_logger.warning("No formatter found for the 'subprocess' case, use default hereinafter") return super().format(record) class LogPipe(threading.Thread): + """ + The thread combined with a context manager to provide a nice way to temporarily redirect something's stream output + into logging module. The most straightforward application is to suppress subprocess STDOUT and/or STDERR streams and + wrap them in the logging mechanism as it is for now for any other message in your app. + """ def __init__(self, logger: logging.Logger, level: int, *args, **kwargs): - """Setup the object with a logger and a loglevel - and start the thread - """ super().__init__(*args, **kwargs) self.logger = logger self.level = level - self.fd_read, self.fd_write = os.pipe() + self.fd_read, self.fd_write = os.pipe() # create 2 ends of the pipe and setup the reading one self.pipe_reader = os.fdopen(self.fd_read) - def __enter__(self): + def __enter__(self) -> int: + """ + Activate the thread and return the consuming end of the pipe so the invoking code can use it to feed its + messages from now on + """ self.start() return self.fd_write def run(self): - """Run the thread, logging everything. + """ + Routine of the thread, logging everything """ for line in iter(self.pipe_reader.readline, ''): - self.logger.log(self.level, line.strip('\n'), 'from_subprocess') + self.logger.log(self.level, line.strip('\n'), 'from_subprocess') # mark the message origin self.pipe_reader.close() def __exit__(self, exc_type, exc_val, exc_tb): """ - The exception will be passed forward, if present, so we don't need to do something with that. The tear-down - process will be done anyway + The exception will be passed forward, if present, so we don't need to do something with that. The following + tear-down process will be done anyway """ os.close(self.fd_write) -def get_platformio_boards(): + +def get_platformio_boards() -> list: + """ + Use PlatformIO Python sources to obtain the boards list. As we interested only in STM32 ones, cut off all the + others. + + IMPORTANT NOTE: The inner implementation can go to the Internet from time to time when it decides that its cache is + out of date. So it can take a long time to execute. + """ + pm = PlatformManager() - return [b['id'] for b in pm.get_all_boards() if 'stm32cube' in b['frameworks']] + return [board['id'] for board in pm.get_all_boards() if 'stm32cube' in board['frameworks']] From 3c9f9caaa5a944cdd628b10945c9f132512c952d Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 26 Feb 2020 01:26:10 +0300 Subject: [PATCH 43/54] more docs --- TODO.md | 3 + stm32pio-gui/app.py | 212 +++++++++++++++++++++++++----------- stm32pio-gui/icons/icon.svg | 1 + stm32pio-gui/main.qml | 12 +- stm32pio/lib.py | 14 +-- stm32pio/settings.py | 2 +- stm32pio/util.py | 3 +- 7 files changed, 167 insertions(+), 80 deletions(-) create mode 100644 stm32pio-gui/icons/icon.svg diff --git a/TODO.md b/TODO.md index 4158645..0d7051a 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,7 @@ - [ ] 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`) - [ ] GUI. Tests + - [ ] GUI. Logging of the internal processes (module-level logger) - [ ] VSCode plugin - [x] Remove casts to string where we can use path-like objects (also 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 @@ -27,4 +28,6 @@ - [ ] 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 - [ ] 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), probably should somehow analyze the output diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 91a84ea..5d6f12b 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from __future__ import annotations +# from __future__ import annotations import collections import logging @@ -11,26 +11,28 @@ import time import weakref -from PySide2.QtCore import QCoreApplication, QUrl, Property, QAbstractListModel, QModelIndex, \ - QObject, Qt, Slot, Signal, QTimer, QThread, qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, \ - QtFatalMsg, QThreadPool, QRunnable, QStringListModel, QSettings -# for Manjaro -# from PySide2.QtWidgets import QApplication -from PySide2.QtGui import QGuiApplication -from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine - - 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, QIcon +from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine + + -class LoggingHandler(logging.Handler): - def __init__(self, buffer): +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 @@ -39,15 +41,27 @@ def emit(self, record: logging.LogRecord) -> None: class LoggingWorker(QObject): - addLog = Signal(str, int) + """ + 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. - def __init__(self, logger): + 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): super().__init__(parent=None) self.buffer = collections.deque() self.stopped = threading.Event() self.can_flush_log = threading.Event() - self.logging_handler = LoggingHandler(self.buffer) + self.logging_handler = BufferedLoggingHandler(self.buffer) logger.addHandler(self.logging_handler) self.logging_handler.setFormatter(stm32pio.util.DispatchingFormatter( @@ -60,36 +74,50 @@ def __init__(self, logger): self.thread.started.connect(self.routine) self.thread.start() - def routine(self): + 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(): - try: + if len(self.buffer): record = self.buffer.popleft() - m = self.logging_handler.format(record) - self.addLog.emit(m, record.levelno) - except IndexError: - pass - print('quit logging thread') + self.sendLog.emit(self.logging_handler.format(record), record.levelno) self.thread.quit() class Stm32pio(stm32pio.lib.Stm32pio): + """ + Other project actions are methods of the class but the config saving by default is not. So we define save_config() + method for consistency. + """ + def save_config(self, parameters: dict = None): - # raise Exception('test') + """ + Flushes the internal configparser.ConfigParser to the file. Also, applies parameters from the dict argument, if + it was given. Keys of the dict are section names in a config whereas values are key-value pairs of the items + inside this section. + """ if parameters is not None: for section_name, section_value in parameters.items(): for key, value in section_value.items(): self.config.set(section_name, key, value) - self.config.save() + return self.config.save() class ProjectListItem(QObject): - nameChanged = Signal() + """ + 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']) - actionResult = Signal(str, bool, arguments=['action', 'success']) + + 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) @@ -97,56 +125,71 @@ def __init__(self, project_args: list = None, project_kwargs: dict = None, paren 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.addLog.connect(self.logAdded) + 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) + 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() + self.qml_ready = threading.Event() # the front and the back both should know when each other is initialized - self._finalizer2 = weakref.finalize(self, self.at_exit) + 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): + 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: # print('start to init in python') # time.sleep(3) # if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': # raise Exception("Error during initialization") - self.project = Stm32pio(*args, **kwargs) + self.project = 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)) - self._name = args[0] # FIXME check if available + 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: - # TODO: maybe remove _-values - pass + self._name = 'Project' # successful initialization. These values should not be used anymore + self._state = {} + self._current_stage = 'Initialized' finally: - self.qml_ready.wait() + self.qml_ready.wait() # wait for the GUI to initialized # print('end to init in python') - self.nameChanged.emit() + 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): - print('destroy', self) - self.workers_pool.waitForDone(msecs=-1) - self.logging_worker.stopped.set() - # self.logThread.quit() + print('destroy', self.project) + self.workers_pool.waitForDone(msecs=-1) # wait for all jobs to complete. Currently, we cannot abort them... + self.logging_worker.stopped.set() # stop the logging worker in the end @Property(str, notify=nameChanged) def name(self): @@ -158,7 +201,9 @@ def name(self): @Property('QVariant', notify=stateChanged) def state(self): if self.project is not None: - return { s.name: value for s, value in self.project.state.items() if s != stm32pio.lib.ProjectStage.UNDEFINED } + # 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 @@ -170,31 +215,44 @@ def current_stage(self): else: return self._current_stage + @Slot() - def completed(self): - print('completed from QML') + def qmlLoaded(self): + """ + Event signaling the complete loading of needed frontend components. + """ + # print('completed from QML') self.qml_ready.set() self.logging_worker.can_flush_log.set() + @Slot(str, 'QVariantList') - def run(self, action, args): - # TODO: queue or smth of jobs - worker = NewProjectActionWorker(getattr(self.project, action), args, self.logger) - worker.actionResult.connect(self.stateChanged) - worker.actionResult.connect(self.stageChanged) - worker.actionResult.connect(self.actionResult) + def run(self, action: str, args: list): + """ + Asynchronously perform Stm32pio actions (generate, build, etc.) (dispatch all business logic). - self.workers_pool.start(worker) + Args: + action: method name of the corresponding Stm32pio action + args: list of positional arguments for the action + """ - @Slot() - def test(self): - print('test') + 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 NewProjectActionWorker(QObject, QRunnable): - actionResult = Signal(str, bool, arguments=['action', 'success']) - def __init__(self, func, args=None, logger=None): +class ProjectActionWorker(QObject, QRunnable): + """ + 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): QObject.__init__(self, parent=None) QRunnable.__init__(self) @@ -217,14 +275,23 @@ def run(self): success = True else: success = False - self.actionResult.emit(self.name, success) + self.actionDone.emit(self.name, success) # notify the caller class ProjectsList(QAbstractListModel): - - def __init__(self, projects: list = None, parent=None): + """ + 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 [] self._finalizer = weakref.finalize(self, self.at_exit) @@ -337,10 +404,24 @@ def set(self, key, value): if __name__ == '__main__': + # Use it as a console logger for whatever you want to + module_logger = logging.getLogger('stm32pio') + module_handler = logging.StreamHandler() + module_logger.addHandler(module_handler) + module_logger.setLevel(logging.DEBUG) + + # Apparently Windows version of PySide2 doesn't have QML logging feature turn on so we fill this gap if stm32pio.settings.my_os == 'Windows': qInstallMessageHandler(qt_message_handler) - app = QGuiApplication(sys.argv) + # 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') @@ -379,6 +460,7 @@ def set(self, key, value): engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml')) main_window = engine.rootObjects()[0] + app.setWindowIcon(QIcon('stm32pio-gui/icons/icon.svg')) def on_loading(): boards_model.setStringList(boards) @@ -392,8 +474,8 @@ def on_loading(): projects_model.add(p) main_window.backendLoaded.emit() - loader = NewProjectActionWorker(loading) - loader.actionResult.connect(on_loading) + loader = ProjectActionWorker(loading) + loader.actionDone.connect(on_loading) QThreadPool.globalInstance().start(loader) sys.exit(app.exec_()) 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/main.qml b/stm32pio-gui/main.qml index 997f94f..16882cb 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -34,7 +34,7 @@ ApplicationWindow { if (initInfo[projectIndex] === 2) { delete initInfo[projectIndex]; // index can be reused - projectsModel.getProject(projectIndex).completed(); + projectsModel.getProject(projectIndex).qmlLoaded(); } } @@ -158,7 +158,7 @@ ApplicationWindow { onNameChanged: { loading = false; } - onActionResult: { + onActionDone: { actionRunning = false; } } @@ -441,7 +441,7 @@ ApplicationWindow { id: projActionsButtonGroup buttons: projActionsRow.children signal stateReceived() - signal actionResult(string actionDone, bool success) + signal actionDone(string actionDone, bool success) property bool lock: false onStateReceived: { if (mainWindow.active && (index === projectsWorkspaceView.currentIndex) && !lock) { @@ -466,7 +466,7 @@ ApplicationWindow { } } } - onActionResult: { + onActionDone: { for (let i = 0; i < buttonsModel.count; ++i) { projActionsRow.children[i].enabled = true; } @@ -482,7 +482,7 @@ ApplicationWindow { projectsWorkspaceView.currentIndexChanged.connect(stateReceived); mainWindow.activeChanged.connect(stateReceived); - project.actionResult.connect(actionResult); + project.actionDone.connect(actionDone); } } RowLayout { @@ -627,7 +627,7 @@ ApplicationWindow { } Connections { target: projActionsButtonGroup - onActionResult: { + onActionDone: { if (actionDone === model.action) { if (success) { glow.color = 'lightgreen'; diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 333e37c..6e0fb6f 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -183,7 +183,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction if logger is not None: self.logger = logger else: - self.logger = logging.getLogger(f"{__name__}.{id(self)}") + self.logger = logging.getLogger(f"{__name__}.{id(self)}") # use id() as uniqueness guarantee # The path is a unique identifier of the project so it would be great to remake Stm32pio class as a subclass of # pathlib.Path and then reference it like self and not self.path. It is more consistent also, as now path is @@ -202,7 +202,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction cubemx_ioc_full_filename=self.ioc_file) self.config.set('project', 'cubemx_script_content', cubemx_script_content) - # Given parameter takes precedence over the saved one + # General rule: given parameter takes precedence over the saved one board = '' if 'board' in parameters and parameters['board'] is not None: if parameters['board'] in stm32pio.util.get_platformio_boards(): @@ -364,6 +364,7 @@ def generate_code(self) -> int: self.logger.info("starting to generate a code from the CubeMX .ioc file...") command_arr = [self.config.get('app', 'java_cmd'), '-jar', self.config.get('app', 'cubemx_cmd'), '-q', cubemx_script_name, '-s'] # -q: read the commands from the file, -s: silent performance + # Redirect the output of the subprocess into the logging module (with DEBUG level) with stm32pio.util.LogPipe(self.logger, logging.DEBUG) as log_pipe: result = subprocess.run(command_arr, stdout=log_pipe, stderr=log_pipe) except Exception as e: @@ -371,8 +372,6 @@ def generate_code(self) -> int: finally: pathlib.Path(cubemx_script_name).unlink() - # TODO: doesn't necessarily means the correct generation (e.g. migration dialog has appeared and 'Cancel' was - # chosen), probably should analyze the output if result.returncode == 0: self.logger.info("successful code generation") return result.returncode @@ -448,11 +447,11 @@ def platformio_ini_is_patched(self) -> bool: for patch_key, patch_value in patch_config.items(patch_section): platformio_ini_value = platformio_ini.get(patch_section, patch_key, fallback=None) if platformio_ini_value != patch_value: - self.logger.debug(f"[{patch_section}]{patch_key}: patch value is\n{patch_value}\nbut " - f"platformio.ini contains\n{platformio_ini_value}") + self.logger.debug(f"[{patch_section}]{patch_key}: patch value is\n {patch_value}\nbut " + f"platformio.ini contains\n {platformio_ini_value}") return False else: - self.logger.debug(f"platformio.ini has not {patch_section} section") + self.logger.debug(f"platformio.ini has no '{patch_section}' section") return False return True @@ -493,6 +492,7 @@ def patch(self) -> None: try: shutil.rmtree(self.path.joinpath('include')) + self.logger.debug("'include' folder has been removed") except: self.logger.info("cannot delete 'include' folder", exc_info=self.logger.isEnabledFor(logging.DEBUG)) # Remove 'src' directory too but on case-sensitive file systems 'Src' == 'src' == 'SRC' so we need to check diff --git a/stm32pio/settings.py b/stm32pio/settings.py index a3845ba..1ad0172 100644 --- a/stm32pio/settings.py +++ b/stm32pio/settings.py @@ -50,4 +50,4 @@ config_file_name = 'stm32pio.ini' -log_fieldwidth_function = 26 +log_fieldwidth_function = 26 # TODO: can be calculated actually (longest name diff --git a/stm32pio/util.py b/stm32pio/util.py index 9ca1b37..b112768 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -5,6 +5,7 @@ import logging import os import threading +from typing import List from platformio.managers.platform import PlatformManager @@ -113,7 +114,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): -def get_platformio_boards() -> list: +def get_platformio_boards() -> List[str]: """ Use PlatformIO Python sources to obtain the boards list. As we interested only in STM32 ones, cut off all the others. From 0fd720901890cc959d7ec4240c7aafdeec926213 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 27 Feb 2020 19:34:34 +0300 Subject: [PATCH 44/54] mostly tests for the new functionality --- TODO.md | 1 + stm32pio-gui/main.qml | 2 + stm32pio/lib.py | 6 +-- stm32pio/tests/test.py | 89 +++++++++++++++++++++++++++++------------- stm32pio/util.py | 2 +- 5 files changed, 68 insertions(+), 32 deletions(-) diff --git a/TODO.md b/TODO.md index 0d7051a..4a56cc4 100644 --- a/TODO.md +++ b/TODO.md @@ -31,3 +31,4 @@ - [ ] General algo of merging a given dict of parameters with the saved one - [ ] 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), probably should somehow analyze the output + - [ ] Dispatch tests on several files (too many code actually) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 16882cb..3c1394f 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -379,6 +379,8 @@ ApplicationWindow { text: 'OK' topInset: 15 leftInset: 10 + topPadding: 20 + leftPadding: 18 onClicked: { projectsListView.currentItem.item.actionRunning = true; diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 6e0fb6f..33462a0 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -89,7 +89,7 @@ def __str__(self): @property def current_stage(self) -> ProjectStage: last_consistent_stage = ProjectStage.UNDEFINED - break_found = False + zero_found = False # Search for a consecutive sequence of True's and find the last of them. For example, if the array is # [1,1,1,0,0,0,0] @@ -97,7 +97,7 @@ def current_stage(self) -> ProjectStage: # we should consider 2 as the last index for name, value in self.items(): if value: - if break_found: + if zero_found: # Fall back to the UNDEFINED stage if we have breaks in conditions results array. E.g., for # [1,1,1,0,1,0,0] # we should return UNDEFINED as it doesn't look like a correct set of files actually @@ -106,7 +106,7 @@ def current_stage(self) -> ProjectStage: else: last_consistent_stage = name else: - break_found = True + zero_found = True return last_consistent_stage diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index 74721c8..d3a6065 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -10,7 +10,9 @@ """ import configparser +import contextlib import inspect +import io import pathlib import platform import re @@ -38,7 +40,8 @@ # proceeding) TEST_PROJECT_BOARD = 'nucleo_f031k6' -# Instantiate a temporary folder on every fixture run. It is used across all tests and is deleted on shutdown +# Instantiate a temporary folder on every fixture run. It is used across all the tests and is deleted on shutdown +# automatically temp_dir = tempfile.TemporaryDirectory() FIXTURE_PATH = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) @@ -198,19 +201,19 @@ def test_run_editor(self): } } - for command, name in editors.items(): + for editor, editor_process_names in editors.items(): # Look for the command presence in the system so we test only installed editors if platform.system() == 'Windows': - command_str = f"where {command} /q" + command_str = f"where {editor} /q" else: - command_str = f"command -v {command}" + command_str = f"command -v {editor}" editor_exists = False if subprocess.run(command_str, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: editor_exists = True if editor_exists: - with self.subTest(command=command, name=name[platform.system()]): - project.start_editor(command) + with self.subTest(command=editor, name=editor_process_names[platform.system()]): + project.start_editor(editor) time.sleep(1) # wait a little bit for app to start @@ -223,7 +226,7 @@ def test_run_editor(self): encoding='utf-8') # Or, for Python 3.7 and above: # result = subprocess.run(command_arr, capture_output=True, encoding='utf-8') - self.assertIn(name[platform.system()], result.stdout) + self.assertIn(editor_process_names[platform.system()], result.stdout) def test_init_path_not_found_should_raise(self): """ @@ -380,7 +383,10 @@ def test_current_stage(self): project.clean() self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.EMPTY) - # TODO: should be undefined when the project is messed up + # Should be UNDEFINED when the project is messed up + project.pio_init() + self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.UNDEFINED) + self.assertFalse(project.state.is_consistent) class TestCLI(CustomTestCase): @@ -467,25 +473,24 @@ def test_verbose(self): ^(?=(DEBUG|INFO|WARNING|ERROR|CRITICAL) {0,4})(?=.{8} (?=(build|pio_init|...) {0,26})(?=.{26} [^ ])) """ project = stm32pio.lib.Stm32pio(FIXTURE_PATH, save_on_destruction=False) - methods = [method[0] for method in inspect.getmembers(project, predicate=inspect.ismethod)] - methods.append('main') + methods = [member[0] for member in inspect.getmembers(project, predicate=inspect.ismethod)] + ['main'] - result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, '-v', 'generate', '-d', str(FIXTURE_PATH)], - encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) + buffer_stdout, buffer_stderr = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(buffer_stdout), contextlib.redirect_stderr(buffer_stderr): + return_code = stm32pio.app.main(sys_argv=['-v', 'generate', '-d', str(FIXTURE_PATH)]) - self.assertEqual(result.returncode, 0, msg="Non-zero return code") - # Somehow stderr and not stdout contains the actual output but we check both - self.assertTrue('DEBUG' in result.stderr or 'DEBUG' in result.stdout, - msg="Verbose logging output hasn't been enabled on stderr") + self.assertEqual(return_code, 0, msg="Non-zero return code") + # stderr and not stdout contains the actual output (by default for logging module) + self.assertIn('DEBUG', buffer_stderr.getvalue(), msg="Verbose logging output hasn't been enabled on stderr") # Inject all methods' names in the regex. Inject the width of field in a log format string regex = re.compile("^(?=(DEBUG) {0,4})(?=.{8} (?=(" + '|'.join(methods) + ") {0," + str(stm32pio.settings.log_fieldwidth_function) + "})(?=.{" + - str(stm32pio.settings.log_fieldwidth_function) + "} [^ ]))", - flags=re.MULTILINE) - self.assertGreaterEqual(len(re.findall(regex, result.stderr)), 1, msg="Logs messages doesn't match the format") + str(stm32pio.settings.log_fieldwidth_function) + "} [^ ]))", flags=re.MULTILINE) + self.assertGreaterEqual(len(re.findall(regex, buffer_stderr.getvalue())), 1, + msg="Logs messages doesn't match the format") - self.assertEqual(len(result.stdout), 0, msg="Process has printed something directly into STDOUT bypassing " - "logging") + self.assertEqual(len(buffer_stdout.getvalue()), 0, + msg="Process has printed something directly into STDOUT bypassing logging") def test_non_verbose(self): """ @@ -498,17 +503,19 @@ def test_non_verbose(self): methods = [method[0] for method in inspect.getmembers(project, predicate=inspect.ismethod)] methods.append('main') - result = subprocess.run([PYTHON_EXEC, STM32PIO_MAIN_SCRIPT, 'generate', '-d', str(FIXTURE_PATH)], - encoding='utf-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE) + buffer_stdout, buffer_stderr = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(buffer_stdout), contextlib.redirect_stderr(buffer_stderr): + return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(FIXTURE_PATH)]) - self.assertEqual(result.returncode, 0, msg="Non-zero return code") - self.assertNotIn('DEBUG', result.stderr, msg="Verbose logging output has been enabled on stderr") - self.assertNotIn('DEBUG', result.stdout, msg="Verbose logging output has been enabled on stdout") + self.assertEqual(return_code, 0, msg="Non-zero return code") + self.assertNotIn('DEBUG', buffer_stderr.getvalue(), msg="Verbose logging output has been enabled on stderr") + self.assertNotIn('DEBUG', buffer_stdout.getvalue(), msg="Verbose logging output has been enabled on stdout") regex = re.compile("^(?=(INFO) {0,4})(?=.{8} ((?!( |" + '|'.join(methods) + "))))", flags=re.MULTILINE) - self.assertGreaterEqual(len(re.findall(regex, result.stderr)), 1, msg="Logs messages doesn't match the format") + self.assertGreaterEqual(len(re.findall(regex, buffer_stderr.getvalue())), 1, + msg="Logs messages doesn't match the format") - self.assertNotIn('Starting STM32CubeMX', result.stderr, msg="STM32CubeMX printed its logs") + self.assertNotIn('Starting STM32CubeMX', buffer_stderr.getvalue(), msg="STM32CubeMX printed its logs") def test_init(self): """ @@ -530,6 +537,32 @@ def test_init(self): self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD, msg="'board' has not been set") + def test_status(self): + """ + Test the output returning by the app on a request to the 'status' command + """ + + buffer_stdout = io.StringIO() + with contextlib.redirect_stdout(buffer_stdout), contextlib.redirect_stderr(None): + return_code = stm32pio.app.main(sys_argv=['status', '-d', str(FIXTURE_PATH)]) + + self.assertEqual(return_code, 0, msg="Non-zero return code") + + matches_counter = 0 + last_stage_pos = -1 + for stage in stm32pio.lib.ProjectStage: + # print(str(stage)) + if stage != stm32pio.lib.ProjectStage.UNDEFINED: + match = re.search(r"^[✅❌] " + str(stage) + '$', buffer_stdout.getvalue(), re.MULTILINE) + # print(match) + self.assertTrue(match, msg="Status information was not found on STDOUT") + if match: + matches_counter += 1 + self.assertGreater(match.start(), last_stage_pos, msg="The order of stages is messed up") + last_stage_pos = match.start() + + self.assertEqual(matches_counter, len(stm32pio.lib.ProjectStage) - 1) + if __name__ == '__main__': unittest.main() diff --git a/stm32pio/util.py b/stm32pio/util.py index b112768..8264406 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -14,7 +14,7 @@ -# Do not add or remove any information from the message and simply pass it 'as-is' +# Do not add or remove any information from the message and simply pass it "as-is" special_formatters = { 'subprocess': logging.Formatter('%(message)s') } default_log_record_factory = logging.getLogRecordFactory() From cf9274d24d46605ed6d28a66bcf3a50a9ec58fe9 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 29 Feb 2020 17:08:55 +0300 Subject: [PATCH 45/54] * new tests and refined old ones * replace Config class by Stm32pio methods (fix for weakref.finalize) * updated .ioc file --- TODO.md | 8 ++-- stm32pio-gui/app.py | 44 ++++++--------------- stm32pio-gui/main.qml | 2 +- stm32pio/lib.py | 90 +++++++++++++++++++++++++----------------- stm32pio/tests/test.py | 37 ++++++++++------- 5 files changed, 92 insertions(+), 89 deletions(-) diff --git a/TODO.md b/TODO.md index 4a56cc4..8bc652c 100644 --- a/TODO.md +++ b/TODO.md @@ -7,14 +7,14 @@ - [ ] GUI. Indicate the progress as states goes forward during the run (see `scratch.py`) - [ ] GUI. Tests - [ ] GUI. Logging of the internal processes (module-level logger) + - [ ] GUI. Reduce number of calls to 'state' (many IO operations) - [ ] VSCode plugin - [x] Remove casts to string where we can use path-like objects (also 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 - [ ] 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 - - [x] `status` CLI subcommand, why not?.. - [ ] check for all tools 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 kitchen, e.g. for embedding) + - [ ] generate code docs (help user to understand an internal mechanics, e.g. for embedding) - [ ] handle the project folder renaming/movement to other location and/or describe in README - [ ] colored logs, maybe... - [ ] check logging work when embed stm32pio lib in third-party stuff @@ -26,9 +26,9 @@ - [ ] `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 + - [ ] `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 + - [ ] 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), probably should somehow analyze the output - [ ] Dispatch tests on several files (too many code actually) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 5d6f12b..7ee8bc6 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -87,25 +87,6 @@ def routine(self) -> None: -class Stm32pio(stm32pio.lib.Stm32pio): - """ - Other project actions are methods of the class but the config saving by default is not. So we define save_config() - method for consistency. - """ - - def save_config(self, parameters: dict = None): - """ - Flushes the internal configparser.ConfigParser to the file. Also, applies parameters from the dict argument, if - it was given. Keys of the dict are section names in a config whereas values are key-value pairs of the items - inside this section. - """ - if parameters is not None: - for section_name, section_value in parameters.items(): - for key, value in section_value.items(): - self.config.set(section_name, key, value) - return self.config.save() - - class ProjectListItem(QObject): """ The core functionality class - GUI representation of the Stm32pio project @@ -165,7 +146,7 @@ def init_project(self, *args, **kwargs) -> None: # time.sleep(3) # if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': # raise Exception("Error during initialization") - self.project = Stm32pio(*args, **kwargs) # our slightly tweaked subclass + 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)) @@ -294,10 +275,10 @@ def __init__(self, projects: list = None, parent: QObject = None): """ super().__init__(parent=parent) self.projects = projects if projects is not None else [] - self._finalizer = weakref.finalize(self, self.at_exit) + # self._finalizer = weakref.finalize(self, self.at_exit) @Slot(int, result=ProjectListItem) - def getProject(self, index): + def getProject(self, index: int): if index in range(len(self.projects)): return self.projects[index] @@ -305,16 +286,16 @@ def rowCount(self, parent=None, *args, **kwargs): return len(self.projects) def data(self, index: QModelIndex, role=None): - if role == Qt.DisplayRole: + if role == Qt.DisplayRole or role is None: return self.projects[index.row()] - def add(self, project): + def addProject(self, project: ProjectListItem): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.projects.append(project) self.endInsertRows() @Slot(QUrl) - def addProject(self, path): + def addProjectByPath(self, path: QUrl): self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) project = ProjectListItem(project_args=[path.toLocalFile()], project_kwargs=dict(save_on_destruction=False)) @@ -343,18 +324,15 @@ def removeProject(self, index): settings.beginGroup('app') settings.remove('projects') settings.beginWriteArray('projects') - for index in range(len(self.projects)): - settings.setArrayIndex(index) - settings.setValue('path', str(self.projects[index].project.path)) + for idx in range(len(self.projects)): + settings.setArrayIndex(idx) + settings.setValue('path', str(self.projects[idx].project.path)) settings.endArray() settings.endGroup() self.endRemoveRows() # print('removed') - def at_exit(self): - print('destroy', self) - # self.logger.removeHandler(self.handler) @@ -405,7 +383,7 @@ def set(self, key, value): if __name__ == '__main__': # Use it as a console logger for whatever you want to - module_logger = logging.getLogger('stm32pio') + module_logger = logging.getLogger(__name__) module_handler = logging.StreamHandler() module_logger.addHandler(module_handler) module_logger.setLevel(logging.DEBUG) @@ -471,7 +449,7 @@ def on_loading(): # ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False)) # ] for p in projects: - projects_model.add(p) + projects_model.addProject(p) main_window.backendLoaded.emit() loader = ProjectActionWorker(loading) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index 3c1394f..55932b4 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -216,7 +216,7 @@ ApplicationWindow { QtLabs.FolderDialog { id: addProjectFolderDialog currentFolder: QtLabs.StandardPaths.standardLocations(QtLabs.StandardPaths.HomeLocation)[0] - onAccepted: projectsModel.addProject(folder) + onAccepted: projectsModel.addProjectByPath(folder) } RowLayout { Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 33462a0..784d5ed 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -83,7 +83,8 @@ def __str__(self): Pretty human-readable complete representation of the project state (not including the service one UNDEFINED to not confuse the end-user) """ - return '\n'.join(f"{'✅ ' if stage_value else '❌ '} {str(stage_name)}" + # Need 2 spaces between the icon and the text to look fine + return '\n'.join(f"{'✅' if stage_value else '❌'} {str(stage_name)}" for stage_name, stage_value in self.items() if stage_name != ProjectStage.UNDEFINED) @property @@ -118,35 +119,6 @@ def is_consistent(self) -> bool: return self.current_stage != ProjectStage.UNDEFINED -class Config(configparser.ConfigParser): - """ - A simple subclass that has additional save() method for the better logic encapsulation - """ - - def __init__(self, project: Stm32pio, *args, **kwargs): - """ - Args: - location: project path (where to store the config file) - *args, **kwargs: passes to the parent's constructor - """ - super().__init__(*args, **kwargs) - self.project = project - - def save(self) -> int: - """ - Tries to save the config to the file and gently log if any error occurs - """ - try: - with self.project.path.joinpath(stm32pio.settings.config_file_name).open(mode='w') as config_file: - self.write(config_file) - self.project.logger.debug("stm32pio.ini config file has been saved") - return 0 - except Exception as e: - self.project.logger.warning(f"cannot save the config: {e}", - exc_info=self.project.logger.isEnabledFor(logging.DEBUG)) - return -1 - - class Stm32pio: """ Main class. @@ -192,7 +164,7 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction # subclassable by-design, unfortunately. See https://bugs.python.org/issue24132 self.path = self._resolve_project_path(dirty_path) - self.config = self._load_config_file() + self.config = self._load_config() self.ioc_file = self._find_ioc_file() self.config.set('project', 'ioc_file', str(self.ioc_file)) @@ -215,7 +187,8 @@ def __init__(self, dirty_path: str, parameters: dict = None, save_on_destruction self.config.set('project', 'board', board) if save_on_destruction: - self._finalizer = weakref.finalize(self, self.config.save) + # Save the config on an instance destruction + self._finalizer = weakref.finalize(self, self._save_config, self.config, self.path, self.logger) def __repr__(self): @@ -292,18 +265,18 @@ def _find_ioc_file(self) -> pathlib.Path: return candidates[0] - def _load_config_file(self) -> Config: + def _load_config(self) -> configparser.ConfigParser: """ - Prepare configparser config for the project. First, read the default config and then mask these values with user - ones + Prepare ConfigParser config for the project. First, read the default config and then mask these values with user + ones. Returns: - custom configparser.ConfigParser instance + new configparser.ConfigParser instance """ self.logger.debug(f"searching for {stm32pio.settings.config_file_name}...") - config = Config(self, interpolation=None) + config = configparser.ConfigParser(interpolation=None) # Fill with default values config.read_dict(stm32pio.settings.config_default) @@ -322,6 +295,49 @@ def _load_config_file(self) -> Config: return config + @staticmethod + def _save_config(config: configparser.ConfigParser, path: pathlib.Path, logger: logging.Logger) -> int: + """ + Writes ConfigParser config to the file path and logs using Logger logger. + + We declare this helper function which can be safely invoked by both internal methods and outer code. The latter + case is suitable for using in weakref' finalizer objects as one of its main requirement is to not keep + references to the destroyable object in any of the finalizer argument so the ordinary bound class method does + not fit well. + + Returns: + 0 on success, -1 otherwise + """ + try: + with path.joinpath(stm32pio.settings.config_file_name).open(mode='w') as config_file: + config.write(config_file) + logger.debug("stm32pio.ini config file has been saved") + return 0 + except Exception as e: + logger.warning(f"cannot save the config: {e}", exc_info=logger.isEnabledFor(logging.DEBUG)) + return -1 + + def save_config(self, parameters: dict = None) -> int: + """ + Invokes base _save_config function. Preliminarily, updates the config with given parameters dictionary. It + should has the following format: + { + 'section1_name': { + 'key1': 'value1', + 'key2': 'value2' + }, + ... + } + + Returns: + passes forward _save_config result + """ + if parameters is not None: + for section_name, section_value in parameters.items(): + for key, value in section_value.items(): + self.config.set(section_name, key, value) + return self._save_config(self.config, self.path, self.logger) + @staticmethod def _resolve_project_path(dirty_path: str) -> pathlib.Path: diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index d3a6065..babeb72 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -1,10 +1,10 @@ """ -Make sure the test project tree is clean before running the tests +NOTE: make sure the test project tree is clean before running the tests! -'pyenv' was used to perform tests with different Python versions under Ubuntu: +'pyenv' was used to perform tests with different Python versions (under Ubuntu): https://www.tecmint.com/pyenv-install-and-manage-multiple-python-versions-in-linux/ -To get the test coverage install and use 'coverage': +To get the test coverage install and use 'coverage' package: $ coverage run -m stm32pio.tests.test -b $ coverage html """ @@ -26,6 +26,7 @@ import stm32pio.app import stm32pio.lib import stm32pio.settings +import stm32pio.util STM32PIO_MAIN_SCRIPT: str = inspect.getfile(stm32pio.app) # absolute path to the main stm32pio script @@ -40,7 +41,7 @@ # proceeding) TEST_PROJECT_BOARD = 'nucleo_f031k6' -# Instantiate a temporary folder on every fixture run. It is used across all the tests and is deleted on shutdown +# Instantiate a temporary folder on every test suite run. It is used across all the tests and is deleted on shutdown # automatically temp_dir = tempfile.TemporaryDirectory() FIXTURE_PATH = pathlib.Path(temp_dir.name).joinpath(TEST_PROJECT_PATH.name) @@ -247,7 +248,7 @@ def test_save_config(self): # 'board' is non-default, 'project'-section parameter project = stm32pio.lib.Stm32pio(FIXTURE_PATH, parameters={'board': TEST_PROJECT_BOARD}, save_on_destruction=False) - project.config.save() + project.save_config() self.assertTrue(FIXTURE_PATH.joinpath(stm32pio.settings.config_file_name).is_file(), msg=f"{stm32pio.settings.config_file_name} file hasn't been created") @@ -264,6 +265,13 @@ def test_save_config(self): self.assertEqual(config.get('project', 'board', fallback="Not found"), TEST_PROJECT_BOARD, msg="'board' has not been set") + def test_get_platformio_boards(self): + """ + PlatformIO identifiers of boards are requested using PlatformIO Python API (not sure it can be called public, + though...) + """ + self.assertIsInstance(stm32pio.util.get_platformio_boards(), list) + class TestIntegration(CustomTestCase): """ @@ -365,7 +373,7 @@ def test_current_stage(self): save_on_destruction=False) self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.EMPTY) - project.config.save() + project.save_config() self.assertEqual(project.state.current_stage, stm32pio.lib.ProjectStage.INITIALIZED) project.generate_code() @@ -480,8 +488,11 @@ def test_verbose(self): return_code = stm32pio.app.main(sys_argv=['-v', 'generate', '-d', str(FIXTURE_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") - # stderr and not stdout contains the actual output (by default for logging module) + # stderr and not stdout contains the actual output (by default for the logging module) + self.assertEqual(len(buffer_stdout.getvalue()), 0, + msg="Process has printed something directly into STDOUT bypassing logging") self.assertIn('DEBUG', buffer_stderr.getvalue(), msg="Verbose logging output hasn't been enabled on stderr") + # Inject all methods' names in the regex. Inject the width of field in a log format string regex = re.compile("^(?=(DEBUG) {0,4})(?=.{8} (?=(" + '|'.join(methods) + ") {0," + str(stm32pio.settings.log_fieldwidth_function) + "})(?=.{" + @@ -489,8 +500,7 @@ def test_verbose(self): self.assertGreaterEqual(len(re.findall(regex, buffer_stderr.getvalue())), 1, msg="Logs messages doesn't match the format") - self.assertEqual(len(buffer_stdout.getvalue()), 0, - msg="Process has printed something directly into STDOUT bypassing logging") + self.assertIn('Starting STM32CubeMX', buffer_stderr.getvalue(), msg="STM32CubeMX has not printed its logs") def test_non_verbose(self): """ @@ -508,14 +518,15 @@ def test_non_verbose(self): return_code = stm32pio.app.main(sys_argv=['generate', '-d', str(FIXTURE_PATH)]) self.assertEqual(return_code, 0, msg="Non-zero return code") + # stderr and not stdout contains the actual output (by default for the logging module) self.assertNotIn('DEBUG', buffer_stderr.getvalue(), msg="Verbose logging output has been enabled on stderr") - self.assertNotIn('DEBUG', buffer_stdout.getvalue(), msg="Verbose logging output has been enabled on stdout") + self.assertEqual(len(buffer_stdout.getvalue()), 0, msg="All app output should flow through the logging module") regex = re.compile("^(?=(INFO) {0,4})(?=.{8} ((?!( |" + '|'.join(methods) + "))))", flags=re.MULTILINE) self.assertGreaterEqual(len(re.findall(regex, buffer_stderr.getvalue())), 1, msg="Logs messages doesn't match the format") - self.assertNotIn('Starting STM32CubeMX', buffer_stderr.getvalue(), msg="STM32CubeMX printed its logs") + self.assertNotIn('Starting STM32CubeMX', buffer_stderr.getvalue(), msg="STM32CubeMX has printed its logs") def test_init(self): """ @@ -551,10 +562,8 @@ def test_status(self): matches_counter = 0 last_stage_pos = -1 for stage in stm32pio.lib.ProjectStage: - # print(str(stage)) if stage != stm32pio.lib.ProjectStage.UNDEFINED: - match = re.search(r"^[✅❌] " + str(stage) + '$', buffer_stdout.getvalue(), re.MULTILINE) - # print(match) + match = re.search(r"^[✅❌] {2}" + str(stage) + '$', buffer_stdout.getvalue(), re.MULTILINE) self.assertTrue(match, msg="Status information was not found on STDOUT") if match: matches_counter += 1 From 3caafcd77cc5676119b42f703bf9ae136d7bee9d Mon Sep 17 00:00:00 2001 From: ussserrr Date: Sat, 29 Feb 2020 17:17:22 +0300 Subject: [PATCH 46/54] clean tree --- Apple/Apple.ioc | 94 ------------------- Orange/Orange.ioc | 94 ------------------- Peach/Peach.ioc | 94 ------------------- scratch.py | 35 ------- .../stm32pio-test-project.ioc | 14 +-- 5 files changed, 3 insertions(+), 328 deletions(-) delete mode 100644 Apple/Apple.ioc delete mode 100644 Orange/Orange.ioc delete mode 100644 Peach/Peach.ioc delete mode 100644 scratch.py diff --git a/Apple/Apple.ioc b/Apple/Apple.ioc deleted file mode 100644 index a14f0be..0000000 --- a/Apple/Apple.ioc +++ /dev/null @@ -1,94 +0,0 @@ -#MicroXplorer Configuration settings - do not modify -File.Version=6 -KeepUserPlacement=true -Mcu.Family=STM32F0 -Mcu.IP0=NVIC -Mcu.IP1=RCC -Mcu.IP2=SYS -Mcu.IP3=USART1 -Mcu.IPNb=4 -Mcu.Name=STM32F031K6Tx -Mcu.Package=LQFP32 -Mcu.Pin0=PF0-OSC_IN -Mcu.Pin1=PA2 -Mcu.Pin2=PA13 -Mcu.Pin3=PA14 -Mcu.Pin4=PA15 -Mcu.Pin5=VP_SYS_VS_Systick -Mcu.PinsNb=6 -Mcu.ThirdPartyNb=0 -Mcu.UserConstants= -Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.6.0 -MxDb.Version=DB.5.0.60 -NVIC.ForceEnableDMAVector=true -NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true -PA13.GPIOParameters=GPIO_Label -PA13.GPIO_Label=SWDIO -PA13.Locked=true -PA13.Mode=Serial_Wire -PA13.Signal=SYS_SWDIO -PA14.GPIOParameters=GPIO_Label -PA14.GPIO_Label=SWCLK -PA14.Locked=true -PA14.Mode=Serial_Wire -PA14.Signal=SYS_SWCLK -PA15.GPIOParameters=GPIO_Label -PA15.GPIO_Label=VCP_RX -PA15.Locked=true -PA15.Mode=Asynchronous -PA15.Signal=USART1_RX -PA2.GPIOParameters=GPIO_Label -PA2.GPIO_Label=VCP_TX -PA2.Locked=true -PA2.Mode=Asynchronous -PA2.Signal=USART1_TX -PF0-OSC_IN.Locked=true -PF0-OSC_IN.Mode=HSE-External-Clock-Source -PF0-OSC_IN.Signal=RCC_OSC_IN -PinOutPanel.RotationAngle=0 -ProjectManager.AskForMigrate=true -ProjectManager.BackupPrevious=false -ProjectManager.CompilerOptimize=6 -ProjectManager.ComputerToolchain=false -ProjectManager.CoupleFile=true -ProjectManager.CustomerFirmwarePackage= -ProjectManager.DefaultFWLocation=true -ProjectManager.DeletePrevious=true -ProjectManager.DeviceId=STM32F031K6Tx -ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 -ProjectManager.FreePins=false -ProjectManager.HalAssertFull=false -ProjectManager.HeapSize=0x200 -ProjectManager.KeepUserCode=true -ProjectManager.LastFirmware=true -ProjectManager.LibraryCopy=1 -ProjectManager.MainLocation=Src -ProjectManager.NoMain=false -ProjectManager.PreviousToolchain= -ProjectManager.ProjectBuild=false -ProjectManager.ProjectFileName=Apple.ioc -ProjectManager.ProjectName=Apple -ProjectManager.StackSize=0x400 -ProjectManager.TargetToolchain=Other Toolchains (GPDSC) -ProjectManager.ToolChainLocation= -ProjectManager.UnderRoot=false -ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true -RCC.CECFreq_Value=32786.88524590164 -RCC.FamilyName=M -RCC.HSICECFreq_Value=32786.88524590164 -RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value -RCC.PLLCLKFreq_Value=8000000 -RCC.PLLMCOFreq_Value=8000000 -RCC.TimSysFreq_Value=8000000 -RCC.VCOOutput2Freq_Value=4000000 -USART1.IPParameters=VirtualMode-Asynchronous -USART1.VirtualMode-Asynchronous=VM_ASYNC -VP_SYS_VS_Systick.Mode=SysTick -VP_SYS_VS_Systick.Signal=SYS_VS_Systick -board=NUCLEO-F031K6 -boardIOC=true diff --git a/Orange/Orange.ioc b/Orange/Orange.ioc deleted file mode 100644 index 60e965e..0000000 --- a/Orange/Orange.ioc +++ /dev/null @@ -1,94 +0,0 @@ -#MicroXplorer Configuration settings - do not modify -File.Version=6 -KeepUserPlacement=true -Mcu.Family=STM32F0 -Mcu.IP0=NVIC -Mcu.IP1=RCC -Mcu.IP2=SYS -Mcu.IP3=USART1 -Mcu.IPNb=4 -Mcu.Name=STM32F031K6Tx -Mcu.Package=LQFP32 -Mcu.Pin0=PF0-OSC_IN -Mcu.Pin1=PA2 -Mcu.Pin2=PA13 -Mcu.Pin3=PA14 -Mcu.Pin4=PA15 -Mcu.Pin5=VP_SYS_VS_Systick -Mcu.PinsNb=6 -Mcu.ThirdPartyNb=0 -Mcu.UserConstants= -Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.6.0 -MxDb.Version=DB.5.0.60 -NVIC.ForceEnableDMAVector=true -NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true -PA13.GPIOParameters=GPIO_Label -PA13.GPIO_Label=SWDIO -PA13.Locked=true -PA13.Mode=Serial_Wire -PA13.Signal=SYS_SWDIO -PA14.GPIOParameters=GPIO_Label -PA14.GPIO_Label=SWCLK -PA14.Locked=true -PA14.Mode=Serial_Wire -PA14.Signal=SYS_SWCLK -PA15.GPIOParameters=GPIO_Label -PA15.GPIO_Label=VCP_RX -PA15.Locked=true -PA15.Mode=Asynchronous -PA15.Signal=USART1_RX -PA2.GPIOParameters=GPIO_Label -PA2.GPIO_Label=VCP_TX -PA2.Locked=true -PA2.Mode=Asynchronous -PA2.Signal=USART1_TX -PF0-OSC_IN.Locked=true -PF0-OSC_IN.Mode=HSE-External-Clock-Source -PF0-OSC_IN.Signal=RCC_OSC_IN -PinOutPanel.RotationAngle=0 -ProjectManager.AskForMigrate=true -ProjectManager.BackupPrevious=false -ProjectManager.CompilerOptimize=6 -ProjectManager.ComputerToolchain=false -ProjectManager.CoupleFile=true -ProjectManager.CustomerFirmwarePackage= -ProjectManager.DefaultFWLocation=true -ProjectManager.DeletePrevious=true -ProjectManager.DeviceId=STM32F031K6Tx -ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 -ProjectManager.FreePins=false -ProjectManager.HalAssertFull=false -ProjectManager.HeapSize=0x200 -ProjectManager.KeepUserCode=true -ProjectManager.LastFirmware=true -ProjectManager.LibraryCopy=1 -ProjectManager.MainLocation=Src -ProjectManager.NoMain=false -ProjectManager.PreviousToolchain= -ProjectManager.ProjectBuild=false -ProjectManager.ProjectFileName=Orange.ioc -ProjectManager.ProjectName=Orange -ProjectManager.StackSize=0x400 -ProjectManager.TargetToolchain=Other Toolchains (GPDSC) -ProjectManager.ToolChainLocation= -ProjectManager.UnderRoot=false -ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true -RCC.CECFreq_Value=32786.88524590164 -RCC.FamilyName=M -RCC.HSICECFreq_Value=32786.88524590164 -RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value -RCC.PLLCLKFreq_Value=8000000 -RCC.PLLMCOFreq_Value=8000000 -RCC.TimSysFreq_Value=8000000 -RCC.VCOOutput2Freq_Value=4000000 -USART1.IPParameters=VirtualMode-Asynchronous -USART1.VirtualMode-Asynchronous=VM_ASYNC -VP_SYS_VS_Systick.Mode=SysTick -VP_SYS_VS_Systick.Signal=SYS_VS_Systick -board=NUCLEO-F031K6 -boardIOC=true diff --git a/Peach/Peach.ioc b/Peach/Peach.ioc deleted file mode 100644 index 4e5bdc2..0000000 --- a/Peach/Peach.ioc +++ /dev/null @@ -1,94 +0,0 @@ -#MicroXplorer Configuration settings - do not modify -File.Version=6 -KeepUserPlacement=true -Mcu.Family=STM32F0 -Mcu.IP0=NVIC -Mcu.IP1=RCC -Mcu.IP2=SYS -Mcu.IP3=USART1 -Mcu.IPNb=4 -Mcu.Name=STM32F031K6Tx -Mcu.Package=LQFP32 -Mcu.Pin0=PF0-OSC_IN -Mcu.Pin1=PA2 -Mcu.Pin2=PA13 -Mcu.Pin3=PA14 -Mcu.Pin4=PA15 -Mcu.Pin5=VP_SYS_VS_Systick -Mcu.PinsNb=6 -Mcu.ThirdPartyNb=0 -Mcu.UserConstants= -Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.6.0 -MxDb.Version=DB.5.0.60 -NVIC.ForceEnableDMAVector=true -NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SVC_IRQn=true\:0\:0\:false\:false\:true\:false\:false -NVIC.SysTick_IRQn=true\:0\:0\:false\:false\:true\:true\:true -PA13.GPIOParameters=GPIO_Label -PA13.GPIO_Label=SWDIO -PA13.Locked=true -PA13.Mode=Serial_Wire -PA13.Signal=SYS_SWDIO -PA14.GPIOParameters=GPIO_Label -PA14.GPIO_Label=SWCLK -PA14.Locked=true -PA14.Mode=Serial_Wire -PA14.Signal=SYS_SWCLK -PA15.GPIOParameters=GPIO_Label -PA15.GPIO_Label=VCP_RX -PA15.Locked=true -PA15.Mode=Asynchronous -PA15.Signal=USART1_RX -PA2.GPIOParameters=GPIO_Label -PA2.GPIO_Label=VCP_TX -PA2.Locked=true -PA2.Mode=Asynchronous -PA2.Signal=USART1_TX -PF0-OSC_IN.Locked=true -PF0-OSC_IN.Mode=HSE-External-Clock-Source -PF0-OSC_IN.Signal=RCC_OSC_IN -PinOutPanel.RotationAngle=0 -ProjectManager.AskForMigrate=true -ProjectManager.BackupPrevious=false -ProjectManager.CompilerOptimize=6 -ProjectManager.ComputerToolchain=false -ProjectManager.CoupleFile=true -ProjectManager.CustomerFirmwarePackage= -ProjectManager.DefaultFWLocation=true -ProjectManager.DeletePrevious=true -ProjectManager.DeviceId=STM32F031K6Tx -ProjectManager.FirmwarePackage=STM32Cube FW_F0 V1.11.0 -ProjectManager.FreePins=false -ProjectManager.HalAssertFull=false -ProjectManager.HeapSize=0x200 -ProjectManager.KeepUserCode=true -ProjectManager.LastFirmware=true -ProjectManager.LibraryCopy=1 -ProjectManager.MainLocation=Src -ProjectManager.NoMain=false -ProjectManager.PreviousToolchain= -ProjectManager.ProjectBuild=false -ProjectManager.ProjectFileName=Peach.ioc -ProjectManager.ProjectName=Peach -ProjectManager.StackSize=0x400 -ProjectManager.TargetToolchain=Other Toolchains (GPDSC) -ProjectManager.ToolChainLocation= -ProjectManager.UnderRoot=false -ProjectManager.functionlistsort=1-MX_GPIO_Init-GPIO-false-HAL-true,2-SystemClock_Config-RCC-false-HAL-false,3-MX_USART1_UART_Init-USART1-false-HAL-true -RCC.CECFreq_Value=32786.88524590164 -RCC.FamilyName=M -RCC.HSICECFreq_Value=32786.88524590164 -RCC.IPParameters=CECFreq_Value,FamilyName,HSICECFreq_Value,PLLCLKFreq_Value,PLLMCOFreq_Value,TimSysFreq_Value,VCOOutput2Freq_Value -RCC.PLLCLKFreq_Value=8000000 -RCC.PLLMCOFreq_Value=8000000 -RCC.TimSysFreq_Value=8000000 -RCC.VCOOutput2Freq_Value=4000000 -USART1.IPParameters=VirtualMode-Asynchronous -USART1.VirtualMode-Asynchronous=VM_ASYNC -VP_SYS_VS_Systick.Mode=SysTick -VP_SYS_VS_Systick.Signal=SYS_VS_Systick -board=NUCLEO-F031K6 -boardIOC=true diff --git a/scratch.py b/scratch.py deleted file mode 100644 index a0aa0a5..0000000 --- a/scratch.py +++ /dev/null @@ -1,35 +0,0 @@ -# Second approach -# view = QQuickView() -# view.setResizeMode(QQuickView.SizeRootObjectToView) -# view.rootContext().setContextProperty('projectsModel', projects) -# view.setSource(QUrl('main.qml')) - -import logging - -import stm32pio.lib -import stm32pio.settings -import stm32pio.util - -# s = stm32pio.lib.ProjectState([ -# (stm32pio.lib.ProjectStage.UNDEFINED, True), -# (stm32pio.lib.ProjectStage.EMPTY, True), -# (stm32pio.lib.ProjectStage.INITIALIZED, True), -# (stm32pio.lib.ProjectStage.GENERATED, True), -# (stm32pio.lib.ProjectStage.PIO_INITIALIZED, True), -# (stm32pio.lib.ProjectStage.PATCHED, False), -# (stm32pio.lib.ProjectStage.BUILT, True), -# ]) - -# logger = logging.getLogger('stm32pio') # the root (relatively to the possible outer scope) logger instance -# handler = logging.StreamHandler() -# logger.addHandler(handler) -# special_formatters = {'subprocess': logging.Formatter('%(message)s')} -# logger.setLevel(logging.DEBUG) -# handler.setFormatter(stm32pio.util.DispatchingFormatter( -# f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", -# special=special_formatters)) - -p = stm32pio.lib.Stm32pio('/Users/chufyrev/Documents/GitHub/stm32pio/stm32pio-test-project', - parameters={ 'board': 'nucleo_f031k6' }, save_on_destruction=False) -print(p.state) -print() diff --git a/stm32pio-test-project/stm32pio-test-project.ioc b/stm32pio-test-project/stm32pio-test-project.ioc index 834d9de..90016d5 100644 --- a/stm32pio-test-project/stm32pio-test-project.ioc +++ b/stm32pio-test-project/stm32pio-test-project.ioc @@ -1,6 +1,6 @@ #MicroXplorer Configuration settings - do not modify File.Version=6 -KeepUserPlacement=true +KeepUserPlacement=false Mcu.Family=STM32F0 Mcu.IP0=NVIC Mcu.IP1=RCC @@ -19,8 +19,8 @@ Mcu.PinsNb=6 Mcu.ThirdPartyNb=0 Mcu.UserConstants= Mcu.UserName=STM32F031K6Tx -MxCube.Version=5.4.0 -MxDb.Version=DB.5.0.40 +MxCube.Version=5.6.0 +MxDb.Version=DB.5.0.60 NVIC.ForceEnableDMAVector=true NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false @@ -47,14 +47,6 @@ PA2.GPIO_Label=VCP_TX PA2.Locked=true PA2.Mode=Asynchronous PA2.Signal=USART1_TX -PCC.Checker=false -PCC.Line=STM32F0x1 -PCC.MCU=STM32F031K6Tx -PCC.PartNumber=STM32F031K6Tx -PCC.Seq0=0 -PCC.Series=STM32F0 -PCC.Temperature=25 -PCC.Vdd=3.6 PF0-OSC_IN.Locked=true PF0-OSC_IN.Mode=HSE-External-Clock-Source PF0-OSC_IN.Signal=RCC_OSC_IN From c398cac750a56f208e29f6e0a3c1a9854c3082f4 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Tue, 3 Mar 2020 00:44:25 +0300 Subject: [PATCH 47/54] QML comments --- LICENSE | 2 +- TODO.md | 6 +- stm32pio-gui/README.md | 0 stm32pio-gui/app.py | 162 +++++++++++++++++++++---------------- stm32pio-gui/icons/LICENSE | 1 + stm32pio-gui/main.qml | 138 ++++++++++++++++++------------- stm32pio/lib.py | 3 +- stm32pio/util.py | 4 +- 8 files changed, 189 insertions(+), 127 deletions(-) create mode 100644 stm32pio-gui/README.md create mode 100644 stm32pio-gui/icons/LICENSE 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/TODO.md b/TODO.md index 8bc652c..175ebbf 100644 --- a/TODO.md +++ b/TODO.md @@ -3,11 +3,10 @@ - [ ] 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... - - [ ] 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`) - [ ] GUI. Tests - - [ ] GUI. Logging of the internal processes (module-level logger) - [ ] 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 (also 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 @@ -32,3 +31,6 @@ - [ ] 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), probably should somehow analyze the output - [ ] Dispatch tests on several files (too many code actually) + - [ ] Note on README that projects are not portable (stores values in .ini file) + - [ ] 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) diff --git a/stm32pio-gui/README.md b/stm32pio-gui/README.md new file mode 100644 index 0000000..e69de29 diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index 7ee8bc6..f469112 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -16,7 +16,9 @@ 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 +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 @@ -55,8 +57,8 @@ class property. Stringifies log records using DispatchingFormatter and passes th sendLog = Signal(str, int) - def __init__(self, logger: logging.Logger): - super().__init__(parent=None) + def __init__(self, logger: logging.Logger, parent: QObject = None): + super().__init__(parent=parent) self.buffer = collections.deque() self.stopped = threading.Event() @@ -83,6 +85,7 @@ def routine(self) -> None: 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() @@ -142,10 +145,6 @@ def init_project(self, *args, **kwargs) -> None: **kwargs: keyword arguments of the Stm32pio constructor """ try: - # print('start to init in python') - # time.sleep(3) - # if args[0] == '/Users/chufyrev/Documents/GitHub/stm32pio/Orange': - # raise Exception("Error during initialization") self.project = stm32pio.lib.Stm32pio(*args, **kwargs) # our slightly tweaked subclass except Exception as e: # Error during the initialization @@ -162,15 +161,15 @@ def init_project(self, *args, **kwargs) -> None: self._current_stage = 'Initialized' finally: self.qml_ready.wait() # wait for the GUI to initialized - # print('end to init in python') 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): - print('destroy', self.project) - self.workers_pool.waitForDone(msecs=-1) # wait for all jobs to complete. Currently, we cannot abort them... - self.logging_worker.stopped.set() # stop the logging worker in the end + 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): @@ -190,7 +189,6 @@ def state(self): @Property(str, notify=stageChanged) def current_stage(self): - # print('wants current_stage') if self.project is not None: return str(self.project.state.current_stage) else: @@ -202,7 +200,6 @@ def qmlLoaded(self): """ Event signaling the complete loading of needed frontend components. """ - # print('completed from QML') self.qml_ready.set() self.logging_worker.can_flush_log.set() @@ -228,13 +225,14 @@ def run(self, action: str, args: list): class ProjectActionWorker(QObject, QRunnable): """ - QObject + QRunnable combination. First allows to attach Qt signals, second is compatible with QThreadPool + 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): - QObject.__init__(self, parent=None) + 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 @@ -275,10 +273,13 @@ def __init__(self, projects: list = None, parent: QObject = None): """ super().__init__(parent=parent) self.projects = projects if projects is not None else [] - # self._finalizer = weakref.finalize(self, self.at_exit) @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] @@ -290,15 +291,24 @@ def data(self, index: QModelIndex, role=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)) + project = ProjectListItem(project_args=[path.toLocalFile()], project_kwargs=dict(save_on_destruction=False), parent=self) self.projects.append(project) settings.beginGroup('app') @@ -311,15 +321,14 @@ def addProjectByPath(self, path: QUrl): self.endInsertRows() @Slot(int) - def removeProject(self, index): - # print('pop index', index) - try: - self.projects[index] - except Exception as e: - print(e) - else: + 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) - self.projects.pop(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') settings.remove('projects') @@ -331,40 +340,39 @@ def removeProject(self, index): settings.endGroup() self.endRemoveRows() - # print('removed') -def loading(): - # time.sleep(3) - global boards - boards = ['None'] + stm32pio.util.get_platformio_boards() - - def qt_message_handler(mode, context, message): if mode == QtInfoMsg: - mode = 'Info' + mode = logging.INFO elif mode == QtWarningMsg: - mode = 'Warning' + mode = logging.WARNING elif mode == QtCriticalMsg: - mode = 'critical' + mode = logging.ERROR elif mode == QtFatalMsg: - mode = 'fatal' + mode = logging.CRITICAL else: - mode = 'Debug' - print("%s: %s" % (mode, message)) + mode = logging.DEBUG + qml_logger.log(mode, message) -DEFAULT_SETTINGS = { - 'editor': '', - 'verbose': False -} 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 DEFAULT_SETTINGS.items(): + for key, value in self.DEFAULT_SETTINGS.items(): if not self.contains('app/settings/' + key): self.setValue('app/settings/' + key, value) @@ -377,6 +385,7 @@ 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) @@ -384,12 +393,19 @@ def set(self, key, value): if __name__ == '__main__': # Use it as a console logger for whatever you want to module_logger = logging.getLogger(__name__) - module_handler = logging.StreamHandler() - module_logger.addHandler(module_handler) - module_logger.setLevel(logging.DEBUG) + 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(f'{__name__}.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 @@ -402,10 +418,14 @@ def set(self, key, value): # Used as a settings identifier too app.setOrganizationName('ussserrr') app.setApplicationName('stm32pio') + app.setWindowIcon(QIcon('stm32pio-gui/icons/icon.svg')) - settings = Settings() + 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')): @@ -414,22 +434,22 @@ def set(self, key, value): settings.endArray() settings.endGroup() - engine = QQmlApplicationEngine() + engine = QQmlApplicationEngine(parent=app) qmlRegisterType(ProjectListItem, 'ProjectListItem', 1, 0, 'ProjectListItem') qmlRegisterType(Settings, 'Settings', 1, 0, 'Settings') - projects_model = ProjectsList() + projects_model = ProjectsList(parent=engine) boards = [] - boards_model = QStringListModel() + boards_model = QStringListModel(parent=engine) engine.rootContext().setContextProperty('Logging', { - 'CRITICAL': logging.CRITICAL, - 'ERROR': logging.ERROR, - 'WARNING': logging.WARNING, - 'INFO': logging.INFO, - 'DEBUG': logging.DEBUG, - 'NOTSET': logging.NOTSET + 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) @@ -438,22 +458,28 @@ def set(self, key, value): engine.load(QUrl.fromLocalFile('stm32pio-gui/main.qml')) main_window = engine.rootObjects()[0] - app.setWindowIcon(QIcon('stm32pio-gui/icons/icon.svg')) - def on_loading(): + + # 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)) for path in projects_paths] - # projects = [ - # ProjectListItem(project_args=['Apple'], project_kwargs=dict(save_on_destruction=False)), - # ProjectListItem(project_args=['Orange'], project_kwargs=dict(save_on_destruction=False)), - # ProjectListItem(project_args=['Peach'], project_kwargs=dict(save_on_destruction=False)) - # ] + 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() + main_window.backendLoaded.emit() # inform the GUI - loader = ProjectActionWorker(loading) + 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/main.qml b/stm32pio-gui/main.qml index 55932b4..a814719 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -13,31 +13,17 @@ import Settings 1.0 ApplicationWindow { id: mainWindow visible: true - minimumWidth: 980 + minimumWidth: 980 // comfortable initial size minimumHeight: 300 height: 530 title: 'stm32pio' color: 'whitesmoke' - property Settings settings: appSettings - + /* + Notify the front-end about the end of an initial loading + */ signal backendLoaded() onBackendLoaded: loadingOverlay.close() - - 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(); - } - } - Popup { id: loadingOverlay visible: true @@ -53,6 +39,10 @@ ApplicationWindow { } } + /* + Slightly customized QSettings + */ + property Settings settings: appSettings QtDialogs.Dialog { id: settingsDialog title: 'Settings' @@ -115,6 +105,45 @@ ApplicationWindow { } } + /* + 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' @@ -125,38 +154,51 @@ ApplicationWindow { } } + /* + 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 + 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 + clip: true // crawls under the Add/Remove buttons otherwise highlight: Rectangle { color: 'darkseagreen' } - highlightMoveDuration: 0 + highlightMoveDuration: 0 // turn off animations highlightMoveVelocity: -1 - model: projectsModel + 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 loading: true + property bool initloading: true // initial waiting for the backend-side property bool actionRunning: false property ProjectListItem project: projectsModel.getProject(index) Connections { - target: project // sender + target: project // (newbie hint) sender onNameChanged: { - loading = false; + // Currently, this event is equivalent to the complete initialization of the backend side of the project + initloading = false; } onActionDone: { actionRunning = false; @@ -194,7 +236,7 @@ ApplicationWindow { Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: parent.height Layout.preferredHeight: parent.height - running: projectsListItem.loading || projectsListItem.actionRunning + running: projectsListItem.initloading || projectsListItem.actionRunning } MouseArea { @@ -202,7 +244,7 @@ ApplicationWindow { y: parent.y width: parent.width height: parent.height - enabled: !parent.loading + enabled: !parent.initloading onClicked: { projectsListView.currentIndex = index; projectsWorkspaceView.currentIndex = index; @@ -234,29 +276,16 @@ ApplicationWindow { Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter display: AbstractButton.TextBesideIcon icon.source: 'icons/remove.svg' - onClicked: { - const indexToRemove = projectsListView.currentIndex; - let indexToMoveTo; - if (indexToRemove === (projectsListView.count - 1)) { - if (projectsListView.count === 1) { - indexToMoveTo = -1; - } else { - indexToMoveTo = indexToRemove - 1; - } - } else { - indexToMoveTo = indexToRemove + 1; - } - - projectsListView.currentIndex = indexToMoveTo; - projectsWorkspaceView.currentIndex = indexToMoveTo; - projectsModel.removeProject(indexToRemove); - } + onClicked: moveToNextAndRemove() } } } - // Screen per project + /* + 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 @@ -267,13 +296,17 @@ ApplicationWindow { // 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) - // Init screen or Work screen + /* + Use another one StackLayout to separate Project initialization "screen" and Main one + */ sourceComponent: StackLayout { - currentIndex: -1 + id: mainOrInitScreen // for clarity + currentIndex: -1 // at widget creation we do not show main nor init screen Layout.fillWidth: true Layout.fillHeight: true @@ -299,9 +332,9 @@ ApplicationWindow { const s = Object.keys(state).filter(stateName => state[stateName]); if (s.length === 1 && s[0] === 'EMPTY') { initDialogLoader.active = true; - currentIndex = 0; // show init dialog + mainOrInitScreen.currentIndex = 0; // show init dialog } else { - currentIndex = 1; // show main view + mainOrInitScreen.currentIndex = 1; // show main view } } } @@ -405,7 +438,7 @@ ApplicationWindow { } } - currentIndex = 1; + mainOrInitScreen.currentIndex = 1; initDialogLoader.sourceComponent = undefined; } } @@ -423,10 +456,7 @@ ApplicationWindow { The project will be removed from the app. It will not affect any real content` icon: QtDialogs.StandardIcon.Critical onAccepted: { - const indexToRemove = projectsWorkspaceView.currentIndex; - projectsListView.currentIndex = projectsWorkspaceView.currentIndex + 1; - projectsWorkspaceView.currentIndex = projectsWorkspaceView.currentIndex + 1; - projectsModel.removeProject(indexToRemove); + moveToNextAndRemove(); projActionsButtonGroup.lock = false; } } diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 784d5ed..cac9edd 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -68,7 +68,7 @@ class ProjectState(collections.OrderedDict): ProjectStage.GENERATED: False, ProjectStage.PIO_INITIALIZED: False, ProjectStage.PATCHED: False, - ProjectStage.BUILT: False, + ProjectStage.BUILT: False } It is also extended with additional properties providing useful information such as obtaining the project current stage. @@ -222,6 +222,7 @@ def state(self) -> ProjectState: self.path.joinpath('platformio.ini').stat().st_size > 0] stages_conditions[ProjectStage.PATCHED] = [ platformio_ini_is_patched, not self.path.joinpath('include').is_dir()] + # Hidden folder! Can be not visible in your familiar file manager and cause a confusion stages_conditions[ProjectStage.BUILT] = [ self.path.joinpath('.pio').is_dir() and any([item.is_file() for item in self.path.joinpath('.pio').rglob('*firmware*')])] diff --git a/stm32pio/util.py b/stm32pio/util.py index 8264406..cfe481c 100644 --- a/stm32pio/util.py +++ b/stm32pio/util.py @@ -15,7 +15,9 @@ # Do not add or remove any information from the message and simply pass it "as-is" -special_formatters = { 'subprocess': logging.Formatter('%(message)s') } +special_formatters = { + 'subprocess': logging.Formatter('%(message)s') +} default_log_record_factory = logging.getLogRecordFactory() From 3ef0f6b783685354c86470139d8c4c02a66f1769 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 4 Mar 2020 01:51:51 +0300 Subject: [PATCH 48/54] removeProject fix, QML comments --- stm32pio-gui/app.py | 21 +++++++++++-- stm32pio-gui/main.qml | 70 +++++++++++++++++++++++++++++-------------- stm32pio/app.py | 2 +- 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index f469112..ad0d39c 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -327,16 +327,31 @@ def removeProject(self, index: int): """ 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(self.projects)): + for idx in range(len(settings_projects_list)): settings.setArrayIndex(idx) - settings.setValue('path', str(self.projects[idx].project.path)) + settings.setValue('path', settings_projects_list[idx]) settings.endArray() + settings.endGroup() self.endRemoveRows() @@ -402,7 +417,7 @@ def set(self, key, value): # 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(f'{__name__}.qml') + 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) diff --git a/stm32pio-gui/main.qml b/stm32pio-gui/main.qml index a814719..78f5ad7 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -196,8 +196,8 @@ ApplicationWindow { 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: { - // Currently, this event is equivalent to the complete initialization of the backend side of the project initloading = false; } onActionDone: { @@ -323,15 +323,16 @@ ApplicationWindow { 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 s = Object.keys(state).filter(stateName => state[stateName]); - if (s.length === 1 && s[0] === 'EMPTY') { - initDialogLoader.active = true; + 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 @@ -339,8 +340,11 @@ ApplicationWindow { } } + /* + Prompt a user to perform initial setup + */ Loader { - id: initDialogLoader + id: initScreenLoader active: false sourceComponent: Column { Text { @@ -353,7 +357,7 @@ ApplicationWindow { ComboBox { id: board editable: true - model: boardsModel + model: boardsModel // backend-side (simple string model) textRole: 'display' onAccepted: { focus = false; @@ -364,13 +368,16 @@ ApplicationWindow { onFocusChanged: { if (!focus) { if (find(editText) === -1) { - editText = textAt(0); + editText = textAt(0); // should be 'None' at index 0 } } else { selectAll(); } } } + /* + Trigger full run + */ CheckBox { id: runCheckBox text: 'Run' @@ -389,7 +396,7 @@ ApplicationWindow { target: board onFocusChanged: { if (!board.focus) { - if (board.editText === board.textAt(0)) { + if (board.editText === board.textAt(0)) { // should be 'None' at index 0 runCheckBox.checked = false; runCheckBox.enabled = false; } else { @@ -415,6 +422,7 @@ ApplicationWindow { topPadding: 20 leftPadding: 18 onClicked: { + // All operations will be queued projectsListView.currentItem.item.actionRunning = true; project.run('save_config', [{ @@ -438,8 +446,8 @@ ApplicationWindow { } } - mainOrInitScreen.currentIndex = 1; - initDialogLoader.sourceComponent = undefined; + mainOrInitScreen.currentIndex = 1; // go to main screen + initScreenLoader.sourceComponent = undefined; // destroy init screen } } } @@ -449,8 +457,11 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true + /* + Detect and reflect changes of a project outside of the app + */ QtDialogs.MessageDialog { - // TODO: .ioc file can be removed on init stage too (i.e. when initDialog is active) + // 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.
The project will be removed from the app. It will not affect any real content` @@ -461,6 +472,9 @@ ApplicationWindow { } } + /* + Show this or action buttons + */ Text { id: initErrorMessage visible: false @@ -469,13 +483,20 @@ ApplicationWindow { color: 'red' } + /* + The core widget - a group of buttons mapping all main actions that can be performed on the given project. + They also serve the project state displaying - each button indicates a stage associated with it: + - green: done + - yellow: in progress right now + - red: an error has occured during the last execution + */ ButtonGroup { id: projActionsButtonGroup buttons: projActionsRow.children signal stateReceived() signal actionDone(string actionDone, bool success) - property bool lock: false - onStateReceived: { + property bool lock: false // TODO: is it necessary? mb make a dialog modal or smth. + onStateReceived: { // TODO: cache state! if (mainWindow.active && (index === projectsWorkspaceView.currentIndex) && !lock) { const state = project.state; project.stageChanged(); @@ -510,6 +531,10 @@ ApplicationWindow { } } Component.onCompleted: { + // Several events lead to a single handler: + // - the state has changed and explicitly informs about it + // - the project was selected in the list + // - the app window has got the focus project.stateChanged.connect(stateReceived); projectsWorkspaceView.currentIndexChanged.connect(stateReceived); mainWindow.activeChanged.connect(stateReceived); @@ -521,7 +546,7 @@ ApplicationWindow { id: projActionsRow Layout.fillWidth: true Layout.bottomMargin: 7 - z: 1 + z: 1 // for the glowing animation Repeater { model: ListModel { id: buttonsModel @@ -533,7 +558,7 @@ ApplicationWindow { ListElement { name: 'Open editor' action: 'start_editor' - margin: 15 // margin to visually separate actions as they doesn't represent any state + margin: 15 // margin to visually separate first 2 actions as they doesn't represent any state } ListElement { name: 'Initialize' @@ -574,12 +599,12 @@ ApplicationWindow { delegate: Button { text: name Layout.rightMargin: model.margin - enabled: false + enabled: false // turn on after project initialization property alias glowVisible: glow.visible function runOwnAction() { projectsListView.currentItem.item.actionRunning = true; palette.button = 'gold'; - let args = []; + let args = []; // JS array cannot be attached to a ListElement (at least in a non-hacky manner) if (model.action === 'start_editor') { args.push(settings.get('editor')); } @@ -588,6 +613,9 @@ ApplicationWindow { onClicked: { runOwnAction(); } + /* + Detect modifier keys. See status bar for information + */ MouseArea { anchors.fill: parent hoverEnabled: true @@ -671,7 +699,7 @@ ApplicationWindow { if (model.shouldRunNext) { model.shouldRunNext = false; - projActionsRow.children[index + 1].clicked(); // complete task + projActionsRow.children[index + 1].clicked(); } if (model.shouldStartEditor) { @@ -694,11 +722,7 @@ ApplicationWindow { glowRadius: 20 spread: 0.25 onVisibleChanged: { - if (visible) { - glowAnimation.start(); - } else { - glowAnimation.complete(); - } + visible ? glowAnimation.start() : glowAnimation.complete(); } SequentialAnimation { id: glowAnimation diff --git a/stm32pio/app.py b/stm32pio/app.py index aaf3011..bdf1f58 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__version__ = '0.96' +__version__ = '1.0' import argparse import logging From dfeb03322c9d62ad7fef4163167964174ca8ec10 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Wed, 4 Mar 2020 19:33:14 +0300 Subject: [PATCH 49/54] README, docs --- README.md | 27 ++++++++++++++++++++------- stm32pio-gui/README.md | 15 +++++++++++++++ stm32pio-gui/main.qml | 28 +++++++++++++++------------- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index dd1511e..d3d1d96 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,16 @@ It uses STM32CubeMX to generate a HAL-framework based code and alongside creates - Start the new project in a single directory using only an `.ioc` file - Update existing project after changing hardware options from 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 more information) ## 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.) @@ -27,20 +30,29 @@ A general recommendation there would be to try to generate and build a code manu ## 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. + +It is also possible 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 ```shell script -$ pip3 uninstall stm32pio +$ pip uninstall stm32pio ``` @@ -58,7 +70,7 @@ $ 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. +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 @@ -95,15 +107,16 @@ You can also use stm32pio as a package and embed it in your own application. See ```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 necessary stuff in CubeMX and run `generate` command in a similar way: ```shell script $ python3 app.py 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 ``` diff --git a/stm32pio-gui/README.md b/stm32pio-gui/README.md index e69de29..a135f67 100644 --- a/stm32pio-gui/README.md +++ 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. 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/main.qml b/stm32pio-gui/main.qml index 78f5ad7..3129a79 100644 --- a/stm32pio-gui/main.qml +++ b/stm32pio-gui/main.qml @@ -614,7 +614,9 @@ ApplicationWindow { runOwnAction(); } /* - Detect modifier keys. See status bar for information + Detect modifier keys: + - Ctrl: start the editor after an operation(s) + - Shift: continuous actions run */ MouseArea { anchors.fill: parent @@ -647,20 +649,12 @@ ApplicationWindow { parent.clicked(); // propagateComposedEvents doesn't work... } onPositionChanged: { - if (mouse.modifiers & Qt.ControlModifier) { - ctrlPressed = true; - } else { - ctrlPressed = false; - } + ctrlPressed = mouse.modifiers & Qt.ControlModifier; // bitwise AND if (ctrlPressedLastState !== ctrlPressed) { ctrlPressedLastState = ctrlPressed; } - if (mouse.modifiers & Qt.ShiftModifier) { - shiftPressed = true; - } else { - shiftPressed = false; - } + shiftPressed = mouse.modifiers & Qt.ShiftModifier; // bitwise AND if (shiftPressedLastState !== shiftPressed) { shiftPressedLastState = shiftPressed; shiftHandler(); @@ -706,7 +700,8 @@ ApplicationWindow { model.shouldStartEditor = false; for (let i = 0; i < buttonsModel.count; ++i) { if (buttonsModel.get(i).action === 'start_editor') { - projActionsRow.children[i].runOwnAction(); // no additional actions in outer handlers + // Use runOwnAction for no additional actions in parent handlers + projActionsRow.children[i].runOwnAction(); break; } } @@ -714,6 +709,9 @@ ApplicationWindow { } } } + /* + Blinky glowing + */ RectangularGlow { id: glow visible: false @@ -757,7 +755,7 @@ ApplicationWindow { selectByMouse: true wrapMode: Text.WordWrap font.family: 'Courier' - font.pointSize: 12 + font.pointSize: 10 // different on different platforms, Qt's bug textFormat: TextEdit.RichText } } @@ -770,6 +768,10 @@ ApplicationWindow { } } + /* + Simple text line. Currently, doesn't support smart intrinsic properties as a fully-fledged status bar, + but is used only for a single feature so not a big deal + */ footer: Text { id: statusBar padding: 10 From 0a159d5d68a177de9aaa9fc8b918c8f960a77a35 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 5 Mar 2020 10:57:21 +0300 Subject: [PATCH 50/54] README filling --- README.md | 53 ++++++++++++++++++++++++++----------------------- TODO.md | 23 ++++++++++----------- stm32pio/app.py | 9 ++++----- 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d3d1d96..9620e06 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # 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 @@ -19,14 +19,14 @@ It uses STM32CubeMX to generate a HAL-framework based code and alongside creates ## Requirements: - For this app: - Python 3.6 and above - - platformio + - `platformio` - For usage: - macOS, Linux, Windows - STM32CubeMX (all recent versions) 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) -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 @@ -39,7 +39,7 @@ $ python3 -m stm32pio (we assume python3 and pip3 hereinafter). It is possible to run the app like this from anywhere. -It is also possible to install the utility to be able to run stm32pio from anywhere. Use +However, it's handier to install the utility to be able to run stm32pio from anywhere. Use ```shell script stm32pio-repo/ $ pip3 install . ``` @@ -50,7 +50,7 @@ Finally, the PyPI distribution (starting from v0.95) is available: $ pip install stm32pio ``` -To uninstall run +To uninstall in both cases run ```shell script $ pip uninstall stm32pio ``` @@ -58,9 +58,9 @@ $ 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. + 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 regenerate 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: @@ -68,15 +68,11 @@ Refer to Example section on more detailed steps. If you face off with some error $ 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. - -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 @@ -84,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). +### 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) @@ -101,18 +105,18 @@ 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. 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 necessary stuff in CubeMX and run `generate` command in a similar way: +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 ``` -9. 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 @@ -124,7 +128,7 @@ 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 otherwise it can lead to some cases to fail. +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 ```shell script @@ -134,8 +138,6 @@ stm32pio-repo/ $ python3 -m unittest stm32pio.tests.test.TestCLI.test_verbose 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. @@ -144,3 +146,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 175ebbf..6c81101 100644 --- a/TODO.md +++ b/TODO.md @@ -3,26 +3,22 @@ - [ ] 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... - - [ ] GUI. Indicate the progress as states goes forward during the run (see `scratch.py`) - - [ ] GUI. Tests + - [ ] 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 (also related to Python version as new ones receive path-like objects arguments) + - [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 - [ ] 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 - - [ ] check for all tools to be present in the system (both CLI and GUI) + - [ ] 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) - - [ ] handle the project folder renaming/movement to other location and/or describe in README + - [ ] generate code docs (help user to understand an internal mechanics, e.g. for embedding). Can be uploaded to the GitHub Wiki - [ ] colored logs, maybe... - - [ ] check logging work when embed stm32pio lib in third-party stuff - - [ ] logging process coverage in README + - [ ] 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 - - [ ] maybe move `_load_config_file()` to `Config` (i.e. `config.load()`) - - [ ] handle the case when the `.ioc` file is set in `stm32pio.ini` but not present in the file system anymore - - [ ] `stm32pio.ini` config file validation + - [ ] 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 @@ -31,6 +27,9 @@ - [ ] 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), probably should somehow analyze the output - [ ] Dispatch tests on several files (too many code actually) - - [ ] Note on README that projects are not portable (stores values in .ini file) + - [ ] 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 diff --git a/stm32pio/app.py b/stm32pio/app.py index bdf1f58..9e73037 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -134,20 +134,19 @@ def main(sys_argv=None) -> int: if args.editor: project.start_editor(args.editor) - elif args.subcommand == 'clean': - project = stm32pio.lib.Stm32pio(args.project_path, save_on_destruction=False) - project.clean() - elif args.subcommand == 'status': project = stm32pio.lib.Stm32pio(args.project_path, save_on_destruction=False) print(project.state) + elif args.subcommand == 'clean': + project = stm32pio.lib.Stm32pio(args.project_path, save_on_destruction=False) + project.clean() + # Library is designed to throw the exception in bad cases so we catch here globally except Exception as e: logger.exception(e, exc_info=logger.isEnabledFor(logging.DEBUG)) return -1 - logger.info("exiting...") return 0 From 1fe3408a875c12e61ea276b14884d096116aae0c Mon Sep 17 00:00:00 2001 From: ussserrr Date: Thu, 5 Mar 2020 16:18:19 +0300 Subject: [PATCH 51/54] setup.py and docs --- CHANGELOG | 156 --------------------------------------------- CHANGELOG.md | 165 ++++++++++++++++++++++++++++++++++++++++++++++++ MANIFEST.in | 3 + README.md | 10 +-- TODO.md | 3 +- setup.py | 21 ++++-- stm32pio/app.py | 2 +- stm32pio/lib.py | 4 +- 8 files changed, 195 insertions(+), 169 deletions(-) delete mode 100644 CHANGELOG create mode 100644 CHANGELOG.md 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..8d83e2f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,165 @@ +# 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: logging machinery - `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. Expected to become the replacement for all PlatformIO CLI calls + - Changed: Markdown markup for this changelog diff --git a/MANIFEST.in b/MANIFEST.in index 060a7ae..c43da9a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,3 +6,6 @@ include README.md include TODO.md recursive-include screenshots * recursive-include stm32pio-test-project * +include stm32pio-gui/main.qml +include stm32pio-gui/README.md +recursive-include stm32pio-gui/icons * diff --git a/README.md b/README.md index 9620e06..0f6fc49 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ It uses STM32CubeMX to generate a HAL-framework-based code and alongside creates - 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 more information) + - *[optional]* GUI version (beta) (see stm32pio-gui sub-folder for the dedicated README) ## Requirements: @@ -80,7 +80,7 @@ $ python3 app.py --help ``` to see help on available commands. -### Patching +### 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. @@ -88,7 +88,7 @@ For those who want to modify the patch (default one is at [`settings.py`](/stm32 ### 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). +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 @@ -130,7 +130,7 @@ 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 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 @@ -146,4 +146,4 @@ While testing was performed on different Python and OS versions, some older Wind 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 + - 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 6c81101..a16fa55 100644 --- a/TODO.md +++ b/TODO.md @@ -25,7 +25,7 @@ - [ ] `__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), probably should somehow analyze the output + - [ ] 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 @@ -33,3 +33,4 @@ - [ ] 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 005e1fe..53381e8 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ """ To pack: + $ pip3 install wheel $ python3 setup.py sdist bdist_wheel To upload to PyPI: @@ -10,7 +11,7 @@ import stm32pio.app -with open("README.md", 'r') as readme: +with open('README.md', 'r') as readme: long_description = readme.read() setuptools.setup( @@ -19,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=[ @@ -33,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/app.py b/stm32pio/app.py index 9e73037..4382026 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -36,9 +36,9 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: "proceeding") parser_new = subparsers.add_parser('new', help="generate CubeMX code, create PlatformIO project") parser_generate = subparsers.add_parser('generate', help="generate CubeMX code only") + parser_status = subparsers.add_parser('status', help="get the description of the current project state") parser_clean = subparsers.add_parser('clean', help="clean-up the project (WARNING: it deletes ALL content of " "'path' except the .ioc file)") - parser_status = subparsers.add_parser('status', help="get the description of the current project state") # Common subparsers options for p in [parser_init, parser_new, parser_generate, parser_clean, parser_status]: diff --git a/stm32pio/lib.py b/stm32pio/lib.py index cac9edd..0c4a648 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -520,7 +520,7 @@ def patch(self) -> None: except: self.logger.info("cannot delete 'src' folder", exc_info=self.logger.isEnabledFor(logging.DEBUG)) - self.logger.info("Project has been patched") + self.logger.info("project has been patched") def start_editor(self, editor_command: str) -> int: @@ -584,7 +584,7 @@ def clean(self) -> None: if child.name != f"{self.path.name}.ioc": if child.is_dir(): shutil.rmtree(child, ignore_errors=True) - self.logger.debug(f"del {child}") + self.logger.debug(f"del {child}/") elif child.is_file(): child.unlink() self.logger.debug(f"del {child}") From bdf9fa178609c1239ce98d37765de1f140b1841b Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 6 Mar 2020 01:46:32 +0300 Subject: [PATCH 52/54] tests under Linux and Python 3.6.X --- CHANGELOG.md | 14 ++++++++++++-- stm32pio-gui/README.md | 2 +- stm32pio-gui/app.py | 3 ++- stm32pio/app.py | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d83e2f..cbd8773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,7 +159,17 @@ - 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: logging machinery - `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 + - 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. Expected to become the replacement for all PlatformIO CLI calls + - 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/stm32pio-gui/README.md b/stm32pio-gui/README.md index a135f67..d8a1ac4 100644 --- a/stm32pio-gui/README.md +++ b/stm32pio-gui/README.md @@ -7,7 +7,7 @@ Currently, it is in a beta stage though all implemented features work, with more ## Installation -The app requires PySide2 5.12+ package. It is available in all major package managers including pip, apt, brew and so on. More convenient installation process is coming in next releases. +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 diff --git a/stm32pio-gui/app.py b/stm32pio-gui/app.py index ad0d39c..57bfc43 100644 --- a/stm32pio-gui/app.py +++ b/stm32pio-gui/app.py @@ -23,7 +23,8 @@ # Most UNIX systems does not provide QtDialogs implementation... from PySide2.QtWidgets import QApplication else: - from PySide2.QtGui import QGuiApplication, QIcon + from PySide2.QtGui import QGuiApplication +from PySide2.QtGui import QIcon from PySide2.QtQml import qmlRegisterType, QQmlApplicationEngine diff --git a/stm32pio/app.py b/stm32pio/app.py index 4382026..d61b0c9 100755 --- a/stm32pio/app.py +++ b/stm32pio/app.py @@ -41,7 +41,7 @@ def parse_args(args: list) -> Optional[argparse.Namespace]: "'path' except the .ioc file)") # Common subparsers options - for p in [parser_init, parser_new, parser_generate, parser_clean, parser_status]: + for p in [parser_init, parser_new, parser_generate, parser_status, parser_clean]: p.add_argument('-d', '--directory', dest='project_path', default=pathlib.Path.cwd(), required=False, help="path to the project (current directory, if not given)") for p in [parser_init, parser_new]: From 9f431b9b57a3cda16e838f56665b0118dbabe013 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 6 Mar 2020 16:48:23 +0300 Subject: [PATCH 53/54] README edit --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0f6fc49..218a2e5 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ It uses STM32CubeMX to generate a HAL-framework-based code and alongside creates - `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 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". @@ -61,7 +61,7 @@ Basically, you need to follow such a pattern: 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 regenerate the code. + 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 @@ -136,8 +136,6 @@ 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. - ## 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. From 8cc03771ab8098eed576c56e1b025f54ef6c2663 Mon Sep 17 00:00:00 2001 From: ussserrr Date: Fri, 6 Mar 2020 18:14:29 +0300 Subject: [PATCH 54/54] use utf-8 symbols in 'status' --- stm32pio/lib.py | 2 +- stm32pio/tests/test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stm32pio/lib.py b/stm32pio/lib.py index 0c4a648..417430e 100644 --- a/stm32pio/lib.py +++ b/stm32pio/lib.py @@ -84,7 +84,7 @@ def __str__(self): not confuse the end-user) """ # Need 2 spaces between the icon and the text to look fine - return '\n'.join(f"{'✅' if stage_value else '❌'} {str(stage_name)}" + return '\n'.join(f"{'[*]' if stage_value else '[ ]'} {str(stage_name)}" for stage_name, stage_value in self.items() if stage_name != ProjectStage.UNDEFINED) @property diff --git a/stm32pio/tests/test.py b/stm32pio/tests/test.py index babeb72..6e1e614 100755 --- a/stm32pio/tests/test.py +++ b/stm32pio/tests/test.py @@ -563,7 +563,7 @@ def test_status(self): last_stage_pos = -1 for stage in stm32pio.lib.ProjectStage: if stage != stm32pio.lib.ProjectStage.UNDEFINED: - match = re.search(r"^[✅❌] {2}" + str(stage) + '$', buffer_stdout.getvalue(), re.MULTILINE) + match = re.search(r"^((\[ \])|(\[\*\])) {2}" + str(stage) + '$', buffer_stdout.getvalue(), re.MULTILINE) self.assertTrue(match, msg="Status information was not found on STDOUT") if match: matches_counter += 1