From 892fede35137f1ff04bd777fa9c34667d73477ff Mon Sep 17 00:00:00 2001 From: Nikhil Sethi Date: Fri, 10 Mar 2023 10:01:37 +0100 Subject: [PATCH 1/2] modules: add basic qt console working mavlink messages in text --- .../modules/mavproxy_qt_console/__init__.py | 126 ++++++++++++++++++ .../modules/mavproxy_qt_console/qt_console.ui | 92 +++++++++++++ .../mavproxy_qt_console/ui_qt_console.py | 82 ++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 MAVProxy/modules/mavproxy_qt_console/__init__.py create mode 100644 MAVProxy/modules/mavproxy_qt_console/qt_console.ui create mode 100644 MAVProxy/modules/mavproxy_qt_console/ui_qt_console.py diff --git a/MAVProxy/modules/mavproxy_qt_console/__init__.py b/MAVProxy/modules/mavproxy_qt_console/__init__.py new file mode 100644 index 0000000000..0d970458ba --- /dev/null +++ b/MAVProxy/modules/mavproxy_qt_console/__init__.py @@ -0,0 +1,126 @@ +from MAVProxy.modules.lib import mp_module +from MAVProxy.modules.mavproxy_qt_console.ui_dialog import Ui_Dialog +from MAVProxy.modules.mavproxy_qt_console.ui_qt_console import Ui_QtConsole +from PySide6.QtWidgets import QApplication, QMainWindow +from PySide6.QtCore import QTimer +from PySide6.QtGui import QColor +import sys +import threading +from MAVProxy.modules.lib import multiproc +import multiprocessing +import time +from MAVProxy.modules.lib.wxconsole_util import Text +import socket +import errno + +class QtConsoleWindow(QMainWindow): + def __init__(self, parent): + super(QtConsoleWindow, self).__init__() + self._parent = parent + self._ui = Ui_QtConsole() + self._ui.setupUi(self) + + self._timer = QTimer(self) + self._timer.timeout.connect(self.update) + self._timer.start(1000) + + def update(self): + '''Slot called by QTimer at a specified interval''' + try: + poll_success = self._parent.child_pipe_recv.poll() + if not poll_success: + return + except socket.error as e: + if e.errno == errno.EPIPE: + self._timer.stop() + return + else: + raise e + + try: + msg = self._parent.child_pipe_recv.recv() + except EOFError: + self._timer.stop() + return + + if isinstance(msg, Text): + self._ui.textEdit.setTextColor(QColor(msg.fg)) + self._ui.textEdit.setTextBackgroundColor(QColor(msg.bg)) + self._ui.textEdit.append(msg.text) + + def accept(self): + print("accepted") + + def reject(self): + self.app.quit() + print("rejected") + +class QtConsole(): + def __init__(self) -> None: + self.parent_pipe_recv, self.child_pipe_send = multiproc.Pipe(duplex=False) + self.child_pipe_recv,self.parent_pipe_send = multiproc.Pipe(duplex=False) + + self.child = multiprocessing.Process(target=self.child_task) + self.child.start() + self.child_pipe_send.close() + self.child_pipe_recv.close() + t = threading.Thread(target=self.watch_thread) + t.daemon = True + # t.start() + + def watch_thread(self): + '''watch for menu events from child''' + from MAVProxy.modules.lib.mp_settings import MPSetting + try: + while True: + msg = self.parent_pipe_recv.recv() + # print(msg) + # if isinstance(msg, win_layout.WinLayout): + # win_layout.set_layout(msg, self.set_layout) + # elif self.menu_callback is not None: + # self.menu_callback(msg) + time.sleep(0.1) + except EOFError: + pass + + def child_task(self): + '''Main Process in which the Qt GUI lives''' + self.parent_pipe_send.close() # Good sense to close pipes that are not used by this process + self.parent_pipe_recv.close() + app = QApplication.instance() + if app == None: + app = QApplication() + + window = QtConsoleWindow(self) + window.show() + app.exec() + + def write(self, text, fg='black', bg='white'): + '''write to the console''' + try: + self.parent_pipe_send.send(Text(text, fg, bg)) + except Exception: + pass + + def writeln(self, text, fg='black', bg='white'): + '''write to the console with linefeed''' + if not isinstance(text, str): + text = str(text) + self.write(text, fg=fg, bg=bg) + +class QtConsoleModule(mp_module.MPModule): + def __init__(self, mpstate): + super().__init__(mpstate, "Qt Console", "GUI Console (Qt)", public=True, multi_vehicle=True) + self.add_command('qt_console', self.cmd_qt_console, "qt console module", ['add','list','remove']) + self.mpstate.console = QtConsole() + + def cmd_qt_console(self, args): + pass + + def mavlink_packet(self, packet): + # print("Packet recieved") + return super().mavlink_packet(packet) + +def init(mpstate): + '''initialise module''' + return QtConsoleModule(mpstate) \ No newline at end of file diff --git a/MAVProxy/modules/mavproxy_qt_console/qt_console.ui b/MAVProxy/modules/mavproxy_qt_console/qt_console.ui new file mode 100644 index 0000000000..4f40d3d150 --- /dev/null +++ b/MAVProxy/modules/mavproxy_qt_console/qt_console.ui @@ -0,0 +1,92 @@ + + + QtConsole + + + + 0 + 0 + 799 + 308 + + + + Qt Console + + + + + + 0 + 40 + 791 + 221 + + + + 0 + + + + Messages + + + + + 0 + 0 + 791 + 191 + + + + true + + + + + + Tab 2 + + + + + + + + 0 + 0 + 799 + 22 + + + + + MAVProxy + + + + + + + + + + + Settings + + + + + Show Map + + + + + Show HUD + + + + + + diff --git a/MAVProxy/modules/mavproxy_qt_console/ui_qt_console.py b/MAVProxy/modules/mavproxy_qt_console/ui_qt_console.py new file mode 100644 index 0000000000..45cb356a17 --- /dev/null +++ b/MAVProxy/modules/mavproxy_qt_console/ui_qt_console.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'qt_console.ui' +## +## Created by: Qt User Interface Compiler version 6.4.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, + QCursor, QFont, QFontDatabase, QGradient, + QIcon, QImage, QKeySequence, QLinearGradient, + QPainter, QPalette, QPixmap, QRadialGradient, + QTransform) +from PySide6.QtWidgets import (QApplication, QMainWindow, QMenu, QMenuBar, + QSizePolicy, QStatusBar, QTabWidget, QTextEdit, + QWidget) + +class Ui_QtConsole(object): + def setupUi(self, QtConsole): + if not QtConsole.objectName(): + QtConsole.setObjectName(u"QtConsole") + QtConsole.resize(799, 308) + self.actionSettings = QAction(QtConsole) + self.actionSettings.setObjectName(u"actionSettings") + self.actionShow_Map = QAction(QtConsole) + self.actionShow_Map.setObjectName(u"actionShow_Map") + self.actionShow_HUD = QAction(QtConsole) + self.actionShow_HUD.setObjectName(u"actionShow_HUD") + self.centralwidget = QWidget(QtConsole) + self.centralwidget.setObjectName(u"centralwidget") + self.tabWidget = QTabWidget(self.centralwidget) + self.tabWidget.setObjectName(u"tabWidget") + self.tabWidget.setGeometry(QRect(0, 40, 791, 221)) + self.tab = QWidget() + self.tab.setObjectName(u"tab") + self.textEdit = QTextEdit(self.tab) + self.textEdit.setObjectName(u"textEdit") + self.textEdit.setGeometry(QRect(0, 0, 791, 191)) + self.textEdit.setReadOnly(True) + self.tabWidget.addTab(self.tab, "") + self.tab_2 = QWidget() + self.tab_2.setObjectName(u"tab_2") + self.tabWidget.addTab(self.tab_2, "") + QtConsole.setCentralWidget(self.centralwidget) + self.menubar = QMenuBar(QtConsole) + self.menubar.setObjectName(u"menubar") + self.menubar.setGeometry(QRect(0, 0, 799, 22)) + self.menuFIle = QMenu(self.menubar) + self.menuFIle.setObjectName(u"menuFIle") + QtConsole.setMenuBar(self.menubar) + self.statusbar = QStatusBar(QtConsole) + self.statusbar.setObjectName(u"statusbar") + QtConsole.setStatusBar(self.statusbar) + + self.menubar.addAction(self.menuFIle.menuAction()) + self.menuFIle.addAction(self.actionSettings) + self.menuFIle.addAction(self.actionShow_Map) + self.menuFIle.addAction(self.actionShow_HUD) + + self.retranslateUi(QtConsole) + + self.tabWidget.setCurrentIndex(0) + + + QMetaObject.connectSlotsByName(QtConsole) + # setupUi + + def retranslateUi(self, QtConsole): + QtConsole.setWindowTitle(QCoreApplication.translate("QtConsole", u"Qt Console", None)) + self.actionSettings.setText(QCoreApplication.translate("QtConsole", u"Settings", None)) + self.actionShow_Map.setText(QCoreApplication.translate("QtConsole", u"Show Map", None)) + self.actionShow_HUD.setText(QCoreApplication.translate("QtConsole", u"Show HUD", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QCoreApplication.translate("QtConsole", u"Messages", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QCoreApplication.translate("QtConsole", u"Tab 2", None)) + self.menuFIle.setTitle(QCoreApplication.translate("QtConsole", u"MAVProxy", None)) + # retranslateUi + From 5d0150c575b2f7a6857021cd3f2c1a356c76c0e7 Mon Sep 17 00:00:00 2001 From: Nikhil Sethi Date: Fri, 10 Mar 2023 21:07:14 +0100 Subject: [PATCH 2/2] modules: add map action and unloading --- .../modules/mavproxy_qt_console/__init__.py | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/MAVProxy/modules/mavproxy_qt_console/__init__.py b/MAVProxy/modules/mavproxy_qt_console/__init__.py index 0d970458ba..9e552803b9 100644 --- a/MAVProxy/modules/mavproxy_qt_console/__init__.py +++ b/MAVProxy/modules/mavproxy_qt_console/__init__.py @@ -1,5 +1,4 @@ from MAVProxy.modules.lib import mp_module -from MAVProxy.modules.mavproxy_qt_console.ui_dialog import Ui_Dialog from MAVProxy.modules.mavproxy_qt_console.ui_qt_console import Ui_QtConsole from PySide6.QtWidgets import QApplication, QMainWindow from PySide6.QtCore import QTimer @@ -9,9 +8,10 @@ from MAVProxy.modules.lib import multiproc import multiprocessing import time -from MAVProxy.modules.lib.wxconsole_util import Text +from MAVProxy.modules.lib.wxconsole_util import Text, Value import socket import errno +from MAVProxy.modules.lib import textconsole class QtConsoleWindow(QMainWindow): def __init__(self, parent): @@ -22,10 +22,17 @@ def __init__(self, parent): self._timer = QTimer(self) self._timer.timeout.connect(self.update) - self._timer.start(1000) + self._timer.start(200) + + self._ui.actionShow_Map.triggered.connect(self.show_map) def update(self): '''Slot called by QTimer at a specified interval''' + if self._parent.close_event.wait(0.001): + self._timer.stop() + self.close() + return + try: poll_success = self._parent.child_pipe_recv.poll() if not poll_success: @@ -47,26 +54,39 @@ def update(self): self._ui.textEdit.setTextColor(QColor(msg.fg)) self._ui.textEdit.setTextBackgroundColor(QColor(msg.bg)) self._ui.textEdit.append(msg.text) + + def show_map(self): + self._parent.child_pipe_send.send("# module load map") - def accept(self): - print("accepted") - - def reject(self): - self.app.quit() - print("rejected") + def closeEvent(self, event) -> None: + """Handles the cross button on the UI""" + if not self._parent.close_event.is_set(): + self._parent.child_pipe_send.send("# module unload qt_console") + return super().closeEvent(event) -class QtConsole(): - def __init__(self) -> None: +class QtConsole(textconsole.SimpleConsole): + def __init__(self, mpstate) -> None: + super(QtConsole, self).__init__() + self.mpstate = mpstate self.parent_pipe_recv, self.child_pipe_send = multiproc.Pipe(duplex=False) self.child_pipe_recv,self.parent_pipe_send = multiproc.Pipe(duplex=False) - + + # For quitting cleanly + self.close_event = multiproc.Event() + self.close_event.clear() + + # main process in which GUI (child) lives self.child = multiprocessing.Process(target=self.child_task) self.child.start() + + # This class (parent) doesn't need the child pipes self.child_pipe_send.close() self.child_pipe_recv.close() + + # Thread that listens to clicks etc. from the GUI t = threading.Thread(target=self.watch_thread) t.daemon = True - # t.start() + t.start() def watch_thread(self): '''watch for menu events from child''' @@ -74,6 +94,8 @@ def watch_thread(self): try: while True: msg = self.parent_pipe_recv.recv() + if msg.startswith("#"): # Header for command packet + self.mpstate.functions.process_stdin(msg[2:]) # print(msg) # if isinstance(msg, win_layout.WinLayout): # win_layout.set_layout(msg, self.set_layout) @@ -100,7 +122,7 @@ def write(self, text, fg='black', bg='white'): try: self.parent_pipe_send.send(Text(text, fg, bg)) except Exception: - pass + pass def writeln(self, text, fg='black', bg='white'): '''write to the console with linefeed''' @@ -108,11 +130,17 @@ def writeln(self, text, fg='black', bg='white'): text = str(text) self.write(text, fg=fg, bg=bg) + def close(self): + '''close the console''' + self.close_event.set() + if self.child.is_alive(): + self.child.join() + class QtConsoleModule(mp_module.MPModule): def __init__(self, mpstate): - super().__init__(mpstate, "Qt Console", "GUI Console (Qt)", public=True, multi_vehicle=True) + super().__init__(mpstate, "qt_console", "GUI Console (Qt)", public=True, multi_vehicle=True) self.add_command('qt_console', self.cmd_qt_console, "qt console module", ['add','list','remove']) - self.mpstate.console = QtConsole() + self.mpstate.console = QtConsole(mpstate) def cmd_qt_console(self, args): pass @@ -121,6 +149,11 @@ def mavlink_packet(self, packet): # print("Packet recieved") return super().mavlink_packet(packet) + def unload(self): + '''unload module''' + self.mpstate.console.close() + self.mpstate.console = textconsole.SimpleConsole() + def init(mpstate): '''initialise module''' return QtConsoleModule(mpstate) \ No newline at end of file