diff --git a/.github/workflows/build-and-release.yaml b/.github/workflows/build-and-release.yaml
index 1597dce..781d12f 100644
--- a/.github/workflows/build-and-release.yaml
+++ b/.github/workflows/build-and-release.yaml
@@ -97,8 +97,8 @@ jobs:
rm -v ./dist/.python_local/Lib/site-packages/PySide6/designer.exe
rm -v ./dist/.python_local/Lib/site-packages/PySide6/qmllint.exe
rm -rv ./dist/.python_local/Lib/site-packages/PySide6/translations
- rm -rv ./dist/.python_local/Lib/site-packages/PySide6/examples
rm -rv ./dist/.python_local/Lib/site-packages/PySide6/qml
+ rm -rv ./dist/.python_local/Lib/site-packages/PySide6/include
- name: Smoke test
run: ./dist/OATFWGUI_Windows.bat --no-gui
diff --git a/OATFWGUI/gui_logic.py b/OATFWGUI/gui_logic.py
index 6921711..141a5c2 100644
--- a/OATFWGUI/gui_logic.py
+++ b/OATFWGUI/gui_logic.py
@@ -7,15 +7,14 @@
from typing import List, Optional
from pathlib import Path
-from PySide6.QtCore import Slot, QThreadPool, QFile, QProcess, Qt
-from PySide6.QtGui import QFont
-from PySide6.QtWidgets import QLabel, QComboBox, QWidget, QFileDialog, QPushButton, QPlainTextEdit, QGridLayout, \
- QHBoxLayout, QCheckBox
+from PySide6.QtCore import Slot, QThreadPool, QFile, QProcess
+from PySide6.QtWidgets import QWidget, QFileDialog
import requests
-from log_utils import LogObject, LoggedExternalFile
-from qt_extensions import Worker, QBusyIndicatorGoodBad, BusyIndicatorState
+from log_utils import LoggedExternalFile
+from qt_extensions import Worker
+from qbusyindicatorgoodbad import BusyIndicatorState
from external_processes import external_processes, get_install_dir
from gui_state import LogicState, PioEnv, FWVersion
from anon_usage_data import AnonStatsDialog, create_anon_stats, upload_anon_stats
@@ -88,7 +87,7 @@ def extract_fw(zipfile_name: Path) -> Path:
class BusinessLogic:
- def __init__(self, main_app: 'MainWidget'):
+ def __init__(self, main_app: QWidget):
self.logic_state = LogicState()
self.main_app = main_app
@@ -368,76 +367,3 @@ def pio_upload_finished(self):
def modal_show_stats(self):
dlg = AnonStatsDialog(self.logic_state, self.main_app)
dlg.exec_()
-
-
-class MainWidget(QWidget):
- def __init__(self, log_object: LogObject):
- QWidget.__init__(self)
-
- # widgets
- self.wMsg_fw_version = QLabel('Select firmware version:')
- self.wCombo_fw_version = QComboBox()
- self.wCombo_fw_version.setPlaceholderText('Grabbing FW Versions...')
- self.wBtn_download_fw = QPushButton('Download')
- self.wBtn_download_fw.setEnabled(False)
- self.wSpn_download = QBusyIndicatorGoodBad(fixed_size=(50, 50))
-
- self.wMsg_pio_env = QLabel('Select board:')
- self.wCombo_pio_env = QComboBox()
- self.wCombo_pio_env.setPlaceholderText('No FW downloaded yet...')
- self.wBtn_select_local_config = QPushButton('Select local config file')
- self.wBtn_build_fw = QPushButton('Build FW')
- self.wBtn_build_fw.setEnabled(False)
- self.wMsg_config_path = QLabel('No config file selected')
- self.wSpn_build = QBusyIndicatorGoodBad(fixed_size=(50, 50))
-
- self.wBtn_refresh_ports = QPushButton('Refresh ports')
- self.wCombo_serial_port = QComboBox()
- self.wCombo_serial_port.setPlaceholderText('No port selected')
- self.wBtn_upload_fw = QPushButton('Upload FW')
- self.wBtn_upload_fw.setEnabled(False)
- self.wSpn_upload = QBusyIndicatorGoodBad(fixed_size=(50, 50))
-
- self.wChk_upload_stats = QCheckBox('Upload anonymous statistics?',
- toolTip='After a successful firmware update, upload anonymous firmware details to the OAT devs')
- self.wBtn_what_stats = QPushButton('What will be uploaded?')
-
- self.logText = QPlainTextEdit()
- self.logText.setLineWrapMode(QPlainTextEdit.NoWrap)
- self.logText.setReadOnly(True)
- log_font = QFont('this-font-does-not-exist')
- log_font.setStyleHint(QFont.Monospace) # Let Qt pick a monospace font
- self.logText.setFont(log_font)
-
- # layout
- self.g_layout = QGridLayout()
-
- layout_arr = [
- [self.wMsg_fw_version, self.wCombo_fw_version, self.wBtn_download_fw, self.wSpn_download],
- [self.wMsg_pio_env, self.wCombo_pio_env, self.wBtn_select_local_config, self.wBtn_build_fw],
- [self.wMsg_config_path, None, None, self.wSpn_build],
- [self.wBtn_refresh_ports, self.wCombo_serial_port, self.wBtn_upload_fw, self.wSpn_upload],
- [None, None, self.wChk_upload_stats, None],
- [None, None, self.wBtn_what_stats, None],
- ]
- for y, row_arr in enumerate(layout_arr):
- for x, widget in enumerate(row_arr):
- rowSpan = 1
- colSpan = 1
- while x + colSpan < len(row_arr) and row_arr[x + colSpan] is None:
- # next widget is None, expand column
- colSpan += 1
- if widget is not None:
- self.g_layout.addWidget(widget, y, x, rowSpan, colSpan)
- self.g_layout.setAlignment(Qt.AlignTop)
-
- # log window will take up the entire right side
- self.h_layout = QHBoxLayout(self)
- self.h_layout.addLayout(self.g_layout)
- self.h_layout.addWidget(self.logText)
-
- # signals
- log_object.log_signal.connect(self.logText.appendHtml)
-
- # business logic will connect signals as well
- self.logic = BusinessLogic(self)
diff --git a/OATFWGUI/main.py b/OATFWGUI/main.py
index b36adf1..790585f 100755
--- a/OATFWGUI/main.py
+++ b/OATFWGUI/main.py
@@ -14,13 +14,14 @@
from typing import Dict, Tuple, Optional
import semver
-from PySide6.QtCore import Slot, Qt
+from PySide6.QtCore import Slot, Qt, QFile
from PySide6.QtWidgets import QApplication, QMainWindow, QStatusBar, QLabel
from PySide6.QtGui import QAction, QActionGroup
+from PySide6.QtUiTools import QUiLoader
from _version import __version__
from log_utils import LogObject, setup_logging
-from gui_logic import MainWidget
+from gui_logic import BusinessLogic
from platform_check import get_platform, PlatformEnum
from external_processes import external_processes, add_external_process, get_install_dir
from anon_usage_data import create_anon_stats
@@ -191,10 +192,24 @@ def __init__(self):
self.bug_hyperlink.setOpenExternalLinks(True)
self.status_bar.addPermanentWidget(self.bug_hyperlink) # addPermanentWidget == right side
- log.debug('Creating main widget')
- self.main_widget = MainWidget(l_o)
+ # Load the main widget from the .ui file
+ # Need to tell the UI loader where our custom widgets are
+ os.environ['PYSIDE_DESIGNER_PLUGINS'] = str(Path(get_install_dir(), 'OATFWGUI'))
+
+ main_widget_ui_path = Path(get_install_dir(), 'OATFWGUI', 'main_widget.ui')
+ log.debug(f'Loading main widget UI from {main_widget_ui_path}')
+ ui_file = QFile(main_widget_ui_path)
+ ui_file.open(QFile.ReadOnly)
+ loader = QUiLoader()
+ self.main_widget = loader.load(ui_file)
+ ui_file.close()
self.setCentralWidget(self.main_widget)
+ # signals
+ l_o.log_signal.connect(self.main_widget.logText.appendHtml)
+ # business logic will connect signals as well
+ self.logic = BusinessLogic(self.main_widget)
+
def add_log_menu_helper(self, name: str, cb_fn, is_checked=False):
action = QAction(name)
action.setCheckable(True)
@@ -266,7 +281,7 @@ def main():
else:
log.debug('NOT executing app')
log.debug('Testing anonymous statistics creation')
- anon_stats = create_anon_stats(widget.main_widget.logic.logic_state)
+ anon_stats = create_anon_stats(widget.logic.logic_state)
log.debug(f'Statistics: {json.dumps(anon_stats)}')
# Wait a bit before exiting, prevents Qt complaining about deleted objects
time.sleep(1.0)
diff --git a/OATFWGUI/main_widget.ui b/OATFWGUI/main_widget.ui
new file mode 100644
index 0000000..8ee85c7
--- /dev/null
+++ b/OATFWGUI/main_widget.ui
@@ -0,0 +1,183 @@
+
+
+ TopLevelWidget
+
+
+ true
+
+
+
+ 0
+ 0
+ 766
+ 513
+
+
+
+ Form
+
+
+ -
+
+
+ QLayout::SetMaximumSize
+
+
-
+
+
-
+
+
+ Select local config file
+
+
+
+ -
+
+
+ What will be uploaded?
+
+
+
+ -
+
+
+ false
+
+
+ Upload FW
+
+
+
+ -
+
+
+ No port selected
+
+
+
+ -
+
+
+ No FW downloaded yet...
+
+
+
+ -
+
+
+ Select firmware version:
+
+
+
+ -
+
+
+ false
+
+
+ Download
+
+
+
+ -
+
+
+ After a successful firmware update, upload anonymous firmware details to the OAT devs
+
+
+ Upload anonymous statistics?
+
+
+
+ -
+
+
+ Select board:
+
+
+
+ -
+
+
+ Grabbing FW Versions...
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ -
+
+
+ false
+
+
+ Build FW
+
+
+
+ -
+
+
+ Refresh ports
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ No config file selected
+
+
+
+
+
+ -
+
+
+
+ Monospace
+
+
+
+ QPlainTextEdit::NoWrap
+
+
+ true
+
+
+
+
+
+
+
+
+
+ QBusyIndicatorGoodBad
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/OATFWGUI/qbusyindicatorgoodbad.py b/OATFWGUI/qbusyindicatorgoodbad.py
new file mode 100644
index 0000000..1a3931a
--- /dev/null
+++ b/OATFWGUI/qbusyindicatorgoodbad.py
@@ -0,0 +1,146 @@
+import math
+import enum
+import logging
+
+from PySide6.QtCore import Qt, QSize
+from PySide6.QtWidgets import QWidget, QStackedWidget, QHBoxLayout, QSizePolicy
+from PySide6.QtGui import QPainter, QColor, QPen
+
+from waitingspinnerwidget import QtWaitingSpinner
+from qt_extensions import RegisteredCustomWidget
+
+log = logging.getLogger('')
+
+
+class BusyIndicatorState(enum.Enum):
+ # designer_dom_xml = '''
+ #
+ #
+ #
+ #
+ # 10
+ # 10
+ #
+ #
+ #
+ #
+ # '''
+ #
+ # def get_fixed_size(self) -> QSize:
+ # return self._fixed_size
+ #
+ # def set_fixed_size(self, fixed_size: QSize):
+ # self._fixed_size = fixed_size
+ # self.setFixedSize(fixed_size)
+ #
+ # fixed_size = Property(QSize, get_fixed_size, set_fixed_size)
+ #
+ # ...
+ #
+ # self._fixed_size = QSize(200, 200)
+ NONE = enum.auto()
+ BUSY = enum.auto()
+ GOOD = enum.auto()
+ BAD = enum.auto()
+
+
+class QBusyIndicatorGoodBad(RegisteredCustomWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.wSpn = QtWaitingSpinner(self, centerOnParent=False)
+ self.wGood = QIndicatorGood(self)
+ self.wBad = QIndicatorBad(self)
+
+ self.wStacked = QStackedWidget()
+ self.wStacked.addWidget(self.wSpn)
+ self.wStacked.addWidget(self.wGood)
+ self.wStacked.addWidget(self.wBad)
+ self.wSpn.start()
+
+ self.hbox = QHBoxLayout(self)
+ self.hbox.addWidget(self.wStacked)
+ self.hbox.setAlignment(Qt.AlignCenter)
+ self.setLayout(self.hbox)
+
+ self.setWindowModality(Qt.NonModal)
+ self.setAttribute(Qt.WA_TranslucentBackground)
+
+ self.setFixedSize(QSize(50, 50))
+
+ self.size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
+ self.setSizePolicy(self.size_policy)
+ if self.running_in_designer():
+ self.setState(BusyIndicatorState.BUSY)
+ else:
+ self.setState(BusyIndicatorState.NONE)
+
+ self.setAttribute(Qt.WA_DontShowOnScreen)
+ self.show()
+
+ def setState(self, state: BusyIndicatorState):
+ if state == BusyIndicatorState.NONE:
+ self.wStacked.hide()
+ elif state == BusyIndicatorState.BUSY:
+ self.wStacked.setCurrentWidget(self.wSpn)
+ self.wStacked.show()
+ # self.wSpn.update()
+ elif state == BusyIndicatorState.GOOD:
+ self.wStacked.setCurrentWidget(self.wGood)
+ self.wStacked.show()
+ elif state == BusyIndicatorState.BAD:
+ self.wStacked.setCurrentWidget(self.wBad)
+ self.wStacked.show()
+ else:
+ log.error(f'Invalid busy indicator state {state}')
+
+
+class QIndicatorGood(QWidget):
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.setWindowModality(Qt.NonModal)
+ self.setAttribute(Qt.WA_TranslucentBackground)
+
+ def paintEvent(self, QPaintEvent):
+ painter = QPainter(self)
+ max_width = painter.device().width()
+ max_height = painter.device().height()
+ bounding_size = min(max_width, max_height)
+ pen = QPen()
+ pen.setWidth(0.05 * bounding_size)
+ pen.setColor(QColor('green'))
+ painter.setPen(pen)
+
+ painter.drawEllipse(0, 0,
+ bounding_size, bounding_size)
+ bottom_point = (0.5 * bounding_size, 0.9 * bounding_size)
+ painter.drawLine(0.2 * bounding_size, 0.6 * bounding_size,
+ *bottom_point)
+ painter.drawLine(*bottom_point,
+ 0.8 * bounding_size, 0.2 * bounding_size)
+
+
+class QIndicatorBad(QWidget):
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.setWindowModality(Qt.NonModal)
+ self.setAttribute(Qt.WA_TranslucentBackground)
+
+ def paintEvent(self, QPaintEvent):
+ painter = QPainter(self)
+ max_width = painter.device().width()
+ max_height = painter.device().height()
+ bounding_size = min(max_width, max_height)
+ pen = QPen()
+ pen.setWidth(0.05 * bounding_size)
+ pen.setColor(QColor('red'))
+ painter.setPen(pen)
+
+ painter.drawEllipse(0, 0,
+ bounding_size, bounding_size)
+ circle_width_fudge = 0.00001 * bounding_size # move the lines into the circle's width just a little
+ on_circle_top_half = (0.5 * math.cos(math.pi * 3 / 4) + 0.5) + circle_width_fudge
+ on_circle_bot_half = (0.5 * math.cos(math.pi * 1 / 4) + 0.5) - circle_width_fudge
+ painter.drawLine(on_circle_top_half * bounding_size, on_circle_top_half * bounding_size,
+ on_circle_bot_half * bounding_size, on_circle_bot_half * bounding_size)
+ painter.drawLine(on_circle_top_half * bounding_size, on_circle_bot_half * bounding_size,
+ on_circle_bot_half * bounding_size, on_circle_top_half * bounding_size)
diff --git a/OATFWGUI/qt_extensions.py b/OATFWGUI/qt_extensions.py
index cd86ce8..6c4a775 100644
--- a/OATFWGUI/qt_extensions.py
+++ b/OATFWGUI/qt_extensions.py
@@ -1,15 +1,10 @@
import sys
import traceback
import logging
-import math
-import enum
-from typing import Optional, Tuple
+from typing import Optional
-from PySide6.QtCore import Slot, Signal, QObject, QRunnable, Qt, QSize, QMetaMethod
-from PySide6.QtWidgets import QWidget, QStackedWidget, QHBoxLayout, QSizePolicy
-from PySide6.QtGui import QPainter, QColor, QPen
-
-from waitingspinnerwidget import QtWaitingSpinner
+from PySide6.QtCore import Slot, Signal, QObject, QRunnable, QMetaMethod
+from PySide6.QtWidgets import QWidget
log = logging.getLogger('')
@@ -48,110 +43,33 @@ def run(self):
self.signals.finished.emit()
-class BusyIndicatorState(enum.Enum):
- NONE = enum.auto()
- BUSY = enum.auto()
- GOOD = enum.auto()
- BAD = enum.auto()
-
-
-class QBusyIndicatorGoodBad(QWidget):
- def __init__(self, fixed_size: Optional[Tuple[int, int]] = None):
- super().__init__()
- self.wSpn = QtWaitingSpinner(self, centerOnParent=False)
- self.wGood = QIndicatorGood(self)
- self.wBad = QIndicatorBad(self)
-
- self.wStacked = QStackedWidget()
- self.wStacked.addWidget(self.wSpn)
- self.wStacked.addWidget(self.wGood)
- self.wStacked.addWidget(self.wBad)
- self.wSpn.start()
-
- self.hbox = QHBoxLayout(self)
- self.hbox.addWidget(self.wStacked)
- self.hbox.setAlignment(Qt.AlignCenter)
- self.setLayout(self.hbox)
-
- self.setWindowModality(Qt.NonModal)
- self.setAttribute(Qt.WA_TranslucentBackground)
-
- if fixed_size is not None:
- self.setFixedSize(QSize(*fixed_size))
- self.size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
- self.setSizePolicy(self.size_policy)
-
- self.setState(BusyIndicatorState.NONE)
- self.setAttribute(Qt.WA_DontShowOnScreen)
- self.show()
-
- def setState(self, state: BusyIndicatorState):
- if state == BusyIndicatorState.NONE:
- self.wStacked.hide()
- elif state == BusyIndicatorState.BUSY:
- self.wStacked.setCurrentWidget(self.wSpn)
- self.wStacked.show()
- self.wSpn.update()
- elif state == BusyIndicatorState.GOOD:
- self.wStacked.setCurrentWidget(self.wGood)
- self.wStacked.show()
- elif state == BusyIndicatorState.BAD:
- self.wStacked.setCurrentWidget(self.wBad)
- self.wStacked.show()
- else:
- log.error(f'Invalid busy indicator state {state}')
-
-
-class QIndicatorGood(QWidget):
- def __init__(self, parent):
- super().__init__(parent)
- self.setWindowModality(Qt.NonModal)
- self.setAttribute(Qt.WA_TranslucentBackground)
-
- def paintEvent(self, QPaintEvent):
- painter = QPainter(self)
- max_width = painter.device().width()
- max_height = painter.device().height()
- bounding_size = min(max_width, max_height)
- pen = QPen()
- pen.setWidth(0.05 * bounding_size)
- pen.setColor(QColor('green'))
- painter.setPen(pen)
-
- painter.drawEllipse(0, 0,
- bounding_size, bounding_size)
- bottom_point = (0.5 * bounding_size, 0.9 * bounding_size)
- painter.drawLine(0.2 * bounding_size, 0.6 * bounding_size,
- *bottom_point)
- painter.drawLine(*bottom_point,
- 0.8 * bounding_size, 0.2 * bounding_size)
-
-
-class QIndicatorBad(QWidget):
- def __init__(self, parent):
- super().__init__(parent)
- self.setWindowModality(Qt.NonModal)
- self.setAttribute(Qt.WA_TranslucentBackground)
-
- def paintEvent(self, QPaintEvent):
- painter = QPainter(self)
- max_width = painter.device().width()
- max_height = painter.device().height()
- bounding_size = min(max_width, max_height)
- pen = QPen()
- pen.setWidth(0.05 * bounding_size)
- pen.setColor(QColor('red'))
- painter.setPen(pen)
-
- painter.drawEllipse(0, 0,
- bounding_size, bounding_size)
- circle_width_fudge = 0.00001 * bounding_size # move the lines into the circle's width just a little
- on_circle_top_half = (0.5 * math.cos(math.pi * 3 / 4) + 0.5) + circle_width_fudge
- on_circle_bot_half = (0.5 * math.cos(math.pi * 1 / 4) + 0.5) - circle_width_fudge
- painter.drawLine(on_circle_top_half * bounding_size, on_circle_top_half * bounding_size,
- on_circle_bot_half * bounding_size, on_circle_bot_half * bounding_size)
- painter.drawLine(on_circle_top_half * bounding_size, on_circle_bot_half * bounding_size,
- on_circle_bot_half * bounding_size, on_circle_top_half * bounding_size)
+class RegisteredCustomWidget(QWidget):
+ """
+ See https://doc.qt.io/qt-6/designer-ui-file-format.html for the XML format (it's kind of hard to read tho :/)
+ Also see https://doc.qt.io/qt-6/designer-creating-custom-widgets.html
+ """
+ designer_tooltip = ''
+ designer_dom_xml = ''
+
+ @classmethod
+ def factory(cls: 'RegisteredCustomWidget'):
+ if not cls.designer_tooltip:
+ cls.designer_tooltip = f'{cls.__name__} tooltip'
+ if cls.designer_dom_xml:
+ cls.designer_dom_xml = f'''
+
+
+
+
+'''
+
+ cls.designer_module = cls.__name__.lower()
+
+ return cls
+
+ def running_in_designer(self):
+ # TODO: This is specific to the .ui file, is there a cleaner way to make it generic?
+ return self.window().objectName() != 'TopLevelWidget'
# https://stackoverflow.com/a/68621792/1313872
diff --git a/OATFWGUI/register_custom_qt_widgets.py b/OATFWGUI/register_custom_qt_widgets.py
new file mode 100644
index 0000000..8bdfbda
--- /dev/null
+++ b/OATFWGUI/register_custom_qt_widgets.py
@@ -0,0 +1,19 @@
+from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
+
+from qbusyindicatorgoodbad import QBusyIndicatorGoodBad
+
+"""
+Set the environment variable PYSIDE_DESIGNER_PLUGINS to this directory and load the plugin,
+both in the app and with pyside6-designer
+
+"""
+
+if __name__ == '__main__':
+ # See RegisteredCustomWidget for this factory paradigm
+ qbusyindicatorgoodbad = QBusyIndicatorGoodBad.factory()
+ QPyDesignerCustomWidgetCollection.registerCustomWidget(
+ qbusyindicatorgoodbad,
+ module=qbusyindicatorgoodbad.designer_module, # idk what this actually affects
+ tool_tip=qbusyindicatorgoodbad.designer_tooltip,
+ xml=qbusyindicatorgoodbad.designer_dom_xml,
+ )
diff --git a/README.md b/README.md
index 73cd35d..5278c41 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ OpenAstroTech FirmWare Graphical User Interface -- A graphical way to build and
## Supported platforms
- Windows 64 bit
- Linux 64 bit
- - Requires Python 3.7+, git, libc >= 2.28 (check with `ldd --version`)
+ - Requires Python 3.7..3.11, git, libc >= 2.28 (check with `ldd --version`)
MacOS _might_ work, don't have a mac to test on. Drop a line if you're willing to test it!
@@ -31,3 +31,20 @@ Windows:
Linux:
![](assets/screenshot_Linux.jpg)
+
+## Development
+To run the app:
+```shell
+$ python3 -m venv .venv # Create a virtual environment in .venv
+$ ./.venv/bin/pip install -r requirements.txt # Install requirements
+$ source .venv/bin/activate && ./OATFWGUI/main.py # Run the app
+```
+
+To run the widget designer you need python development libraries:
+- Fedora: `sudo dnf install python3-devel`
+- Ubuntu (probably, not tested): `sudo apt install python3-dev`
+
+- then:
+`source .venv/bin/activate && env PYSIDE_DESIGNER_PLUGINS=$(realpath ./OATFWGUI) pyside6-designer`
+
+Open `main_widget.ui` in the designer
diff --git a/requirements.txt b/requirements.txt
index 254cbad..088f84c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
platformio==6.1.4
-PySide6-Essentials==6.4.2
+PySide6-Essentials==6.5.3 # Last version that supports python 3.7
requests~=2.28.1
semver~=2.13.0
pygments~=2.13.0
\ No newline at end of file