Overview
The GUI tool is an interface written in PyQt5. It utilizes an instance of the CLI wrapper class to communicate with the CyDAQ. The main code can be found under gui/app.py
(start at the bottom of the file). The class MainWindow is the app's starting point; all other widgets/elements are added. It also contains a thread pool for running commands asynchronously and the CLI Wrapper. Each widget is also its class, which can all be found in the gui/widgets/
directory.
Running the App
If you haven't downloaded the required packages, do so with this command:
python3 -m pip install -r requirements.txt
The app itself can be run with the following command:
python gui/app.py
If you add new changes, they can also be seen with the above app without needing to build anything new. Make sure to save your changes in the other file.
Compiling and Building an EXE file
Simply run the compile.sh
file found in the root directory of the project. It will output an .exe file in the dist/
folder.
./compile.sh
Note: Make sure you can run the app using the above commands first before building.
If you would like to change the current configuration, the documentation to do so can be found here (PyInstaller Docs).
Currently, the compile.sh
file is configured to generate a single .exe file with no external dependency files, with the CyDAQ logo and named CyDAQ.exe
. These are the only things we recommend changing, as changing any of the other arguments may result in the application breaking.
Changing GUI elements in QT Designer
You can open .ui
files (located in gui/qtdesigner) with QT Designer and make any necessary changes to the various pages and widgets.
Once the changes are done, you must re-build the python files (located in gui/generated. We provide a script do do this automatically, and can be ran with the following:
cd gui
bash build.sh
Note: You MUST run this script from within the GUI directory.
There are more instructions later in the document that outline the steps required to add new pages and widgets to the app, and how the build script must be changed to support the new .UI files.
Add a Module
If you want to add a module to the CyDAQ Interface, you must design the page in QtDesigner, build it, and import it into a class file to make it recognizable and accessible from the front page. This also allows for backend logic that can be handled on the module page.
Design the Page
First, you'll need to create the page in QtDesigner from which to import the actual buttons, switches, inputs, outputs, etc., with which you will work with in your code.
- Open QtDesigner
- Create a new file with the type 'Widget'
- Add whichever components, layouts, forms, etc. you want on your page design. Make sure you name each item that you will be using in the backend such as buttons, labels that change, forms, etc.
- Make sure to rename the window to something like new_module in the top right "Object Inspector."
- Save the file in the
gui/qtdesigner/
folder with the name of the new widget. - Add an entry to the
build.sh
andbuild-linux.sh
similar to the following:
build.sh:
python -m PyQt5.uic.pyuic --from-imports qtdesigner/NewModuleName.ui -o generated/NewModuleUI.py
build-linux.sh:
pyuic5 --from-imports qtdesigner/NewModuleName.ui -o generated/NewModuleUI.py
- The following commands must be run to generate the new .py files. The generated files can be found in
generated/
.
cd gui
bash build.sh
or if on linux:
bash build-linux.sh
Creating the Widget Backend
- Create the file in
gui/widgets/
and name it with a lowercase and underscore format similar to what is already in the folder (this is so the file name and class name are not the same) - In the file, name it in the form of a class, and import the MainWindow object and the generated Ui page.
- The code snippet below is how to properly get the module set up and imported from the MainWindow:
from PyQt5 import QtWidgets
from generated.NewModuleUI import Ui_new_module
class NewModuleName(QtWidgets.QWidget, Ui_new_module, CyDAQModeWidget):
def __init__(self, mainWindow):
super(NewModuleName, self).__init__() # Initiates the superclass (parent)
self.setupUi(self) # Used in the generated .py file
# Import parent window and worker object from parent
self.mainWindow = mainWindow
# Share resources from main window
self.threadpool = self.mainWindow.threadpool
self.wrapper = mainWindow.wrapper
self.logger = self.mainWindow.logger
# define other methods below
Note: Check out basic_operation.py in
gui/widgets
for a more in-depth example.
Adding the page to the MainWindow
- Open
gui/app.py
. - At the top, add a new import statement for your new module that you created in the above section with the other imports like this:
from widgets import NewModuleWidget
- Scroll down to the
__init__()
method and find the '# Widgets` comment. - Add a new line with the following format right under that comment:
self.new_module = NewModuleWidget(self)
- A few lines down, you'll see this line:
self.widgets = []
Find the line with the last widget added, and add a new line after that (it's important that you keep the specific order!):
self.widgets.append(self.new_module)
- Lastly, scroll down to the
The following are methods for switching...
comment, and, underneath all of the otherswitchTo...
methods, add a new one with this format:
def switchToNewModule(self):
self.stack.setCurrentIndex(x)
Where 'x' is the next number in the index (check the method above it and add 1 to that number). This tells the application that there is a new page at that index, and to switch to that index specifically when you want to get to that page.
Adding it to the Mode Selector Page
Once you've created the widgets class, we need to assign the page to a button on QtDesigner and in the mode selector widget code. If you already have a button created and ready to be assigned, you can skip to the next section.
Add a Button
- First, open QtDesigner and open the
gui/qtdesigner/ModeSelectorWidget.ui
file, drag a button onto the layout however you choose, and set the text to "New Module," whatever your module name is. - Then, in the top right Object Inspector, set the name of your new QButton to "newModuleBtn" or something along that format.
- Save the file and exit.
- Regenerate the file using the
build.sh
(Windows) orbuild-linux.sh
(Linux) file.
mode_selector.py
File
Adding to the - Open the
gui/widgets/mode_selector.py
file. - Scroll to the
__init__()
method in theModeSelectorWidget
class. - Add code with the following format (replacing your button name) under the
Mode Selector Buttons
comment:
newWidgetButton = self.new_widget_btn
newWidgetButton.setCheckable(True)
newWidgetButton.clicked.connect(lambda: self.mainWindow.switchToNewModule())
Conclusion
You should now have a new module added. You have the .ui design file, the generated .py file from it, the backend module file where the inner logic is run, and now it is hooked into the main window to get it's resources and threadpools as well as accessible from the Mode Selector page.
CyDAQ Communication
The GUI uses the CLI Wrapper class to communicate with the CyDAQ. This class was purposefully built for this project and exists in the same repository. An instance of the CLI Wrapper class holds a running process of the CLI Tool, which also exists in this repository.
Each method of the CLI Wrapper is blocking, which means it needs to be run asynchronously with the main thread of the UI. This is achieved using the existing PyQT5 QRunnable and QThreadPool classes to push the function calls into another thread. This means that long-running functions, like sampling/retrieving data, won't cause the GUI to freeze when run.
The CLI Wrapper class raises custom exceptions when certain errors occur, for example, a sudden disconnect of the CyDAQ. These errors can be caught by the GUI code and handled appropriately.
Logging
We are using the built in logging library for Python to handle terminal, file, and GUI logging. The logger object is first created in the init function of MainWindow in app.py
, then passed to each subsequent widget/window that needs it. The logging object also gets passed to the Wrapper, so all wrapper commands and response are included in logging outputs.
The Debug page uses the logging object to write logs to the GUI. Check out the LogHandler
class in debug.py
for it's implementation.