Source code for trspectrometer.mainwindow
#!/usr/bin/env python3
# Copyright 2020 Patrick C. Tapping
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
# TODO: We want to load the log window before hardware detection etc,
# but at the moment that's really messy. It should be possible to clean it up
# so we're not loading modules inside __init__() etc.
import os
import sys
import importlib
import argparse
import traceback
import logging
from PySide6 import QtWidgets
from PySide6.QtUiTools import loadUiType
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import Signal
import zarr
# This is a workaround for loadUiType not finding the resource _rc.py inside the package
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
from signalstorage import signals
from busydialog import BusyDialog
_log = logging.getLogger(__name__)
[docs]def exception_handler(extype, value, tb):
"""
Global handler for exceptions.
Not particularly smart, but stops crashes from minor recoverable errors.
The exception details are sent to the log.
"""
logging.error("Caught an unhandled exception:\n " + " ".join(traceback.format_exception(extype, value, tb)).rstrip("\n"))
sys.excepthook = exception_handler
[docs]class MainWindow(*loadUiType(__file__.split(".py")[0] + ".ui")):
"""
Main window for the various components of the spectrometer software.
"""
#: Signal to trigger a status bar message.
show_message = Signal(str)
def __init__(self):
super().__init__()
self.setupUi(self)
# Connect the show_message signal to display message on the status bar
self.show_message.connect(self.statusbar.showMessage)
# Note we are doing imports as late as possible so that the logPanel
# is able to catch and display as much as possible from the logger.
# Intialise logging system
from logpanel import LogPanel
self.logPanel = LogPanel()
self.actionLog.triggered.connect(lambda: _show_and_activate(self.logPanel))
# Initialise configuration
import configuration as config
# Initialise data panel
import datastorage as ds
self.ds = ds
from datapanel import DataPanel
self.dataPanel = DataPanel(self)
self.tabWidget.insertTab(1, self.dataPanel, "Data")
# Initialise about panel
from aboutpanel import AboutPanel
self.aboutPanel = AboutPanel(self)
# Start with data panel selected
self.tabWidget.setCurrentIndex(1)
# Connect UI signals
self.actionNew.triggered.connect(self._new_clicked)
self.actionOpen.triggered.connect(self._open_clicked)
self.actionSaveAs.triggered.connect(self._save_clicked)
self.actionImportRawData.triggered.connect(self._import_raw_data_clicked)
self.actionExportRawDataAverage.triggered.connect(self._export_raw_data_average_clicked)
self.actionAbout.triggered.connect(self._about_clicked)
self.actionHardwareStatus.triggered.connect(self._hardwarestatus_clicked)
# Start new data set
self._new_clicked()
[docs] def closeEvent(self, event):
"""Handler for window close event."""
if not self.dataPanel.close():
# Data acquisition in progress, close cancelled
event.ignore()
return
self.logPanel.close()
try:
self.hardwarestatusPanel.close()
except:
pass
import hardware as hw
hw.close()
event.accept()
def _new_clicked(self):
"""Handler for the File->New menu item."""
if not self.ds.prompt_unsaved(parent=self):
return
self.ds.new_raw_zarr(location=None)
signals.data_changed.emit(False) # Don't update UI controls
self.actionSaveAs.setEnabled(False)
self.menuExport.setEnabled(False)
self.actionProperties.setEnabled(False)
self.statusbar.showMessage("New data set ready.")
def _open_clicked(self):
"""Handler for the File->Open menu item."""
if not self.ds.prompt_unsaved(parent=self):
return
try:
dirname = self.ds.open_zarr(parent=self)
except Exception as ex:
_log.error(f"{ex}")
QMessageBox.critical(self, "Error opening data directory", f"{ex}")
return
if not dirname: return
signals.data_changed.emit(True)
self.actionSaveAs.setEnabled(True)
self.menuExport.setEnabled(True)
self.actionProperties.setEnabled(True)
msg = f"Data opened from {dirname}"
_log.info(msg)
self.statusbar.showMessage(msg)
def _save_clicked(self):
"""Handler for the File->Save menu item."""
try:
dirname = self.ds.save_zarr(parent=self)
except Exception as ex:
_log.error(f"{ex}")
QMessageBox.critical(self, "Error saving data directory", f"{ex}")
return
if dirname == None: return
msg = f"Data saved to {dirname}"
_log.info(msg)
self.statusbar.showMessage(msg)
def _import_raw_data_clicked(self):
"""Handler for the File->Import->Raw Data menu item."""
if not self.ds.prompt_unsaved(parent=self):
return
try:
filename, data = self.ds.import_raw(parent=self)
except Exception as ex:
_log.error(f"{ex}")
QMessageBox.critical(self, "Error importing raw data", f"{ex}")
return
if not filename: return
self.ds.d = data
self.ds.t = zarr.group()
signals.data_changed.emit(True)
self.actionSaveAs.setEnabled(True)
self.menuExport.setEnabled(True)
self.actionProperties.setEnabled(True)
msg = f"Data imported from {filename}"
_log.info(msg)
self.statusbar.showMessage(msg)
def _export_raw_data_average_clicked(self):
"""Handler for the File->Export->Raw Data Average menu item."""
try:
filename = self.ds.export_raw_average(parent=self)
except Exception as ex:
_log.exception(f"{ex}")
QMessageBox.critical(self, "Error exporting raw data average", f"{ex}")
return
if filename is None: return
msg = f"Data exported to {filename}"
_log.info(msg)
self.statusbar.showMessage(msg)
def _about_clicked(self):
self.aboutPanel.exec_()
def _hardwarestatus_clicked(self):
import hardware as hw
self.hardwarestatusPanel = hw.HardwareStatusPanel()
self.hardwarestatusPanel.show()
self.hardwarestatusPanel.activateWindow()
def _hardware_init(self):
import hardware as hw
BusyDialog(parent=self, modal=True, title="Initialising Hardware", label="Initialising hardware devices...", task=lambda dialog: hw.init())
def _show_and_activate(widget):
"""Show and activate a given QWidget."""
widget.show()
widget.activateWindow()
[docs]def main():
"""Run the launcher application."""
# Respond to command line arguments
argparser = argparse.ArgumentParser(description="Run the TRSpectrometer application.")
argparser.add_argument("--hardware", help="Perform hardware detection and initialisation.", nargs=1, choices=["true", "false"])
argparser.add_argument("--loglevel", help="Verbosity of the log output.", nargs=1, choices=["critical", "error", "warning", "info", "debug"], default=["info"])
args = argparser.parse_args()
if not args.hardware is None:
args.hardware = (args.hardware[0] == "true")
logging.basicConfig(level=args.loglevel[0].upper())
# Initialise the main application window
app = QtWidgets.QApplication(sys.argv)
mw = MainWindow()
import configuration as config
config.mainwindow = mw
# Load plugin modules from user plugin or builtin plugin locations
for p in config.plugin_dirs()[::-1]:
sys.path.insert(0, os.path.abspath(p))
if config.data["plugins"]["load"]:
_log.info(f"Plugin search path is: {sys.path}")
for p in config.data["plugins"]["load"]:
try:
_log.info(f"Loading plugin module: {p}")
importlib.import_module(p, package=__package__)
except ModuleNotFoundError:
_log.error(f"Module not found for plugin: {p}")
except Exception:
_log.exception(f"Error loading plugin module: {p}")
# Adjust config based on provided command line arguments
if not args.hardware is None:
config.data["hardware"]["init_hardware"] = args.hardware
# Initialise hardware if requested
if config.data["hardware"]["init_hardware"]:
config.mainwindow._hardware_init()
else:
_log.info("Hardware not initialised. Set \"init_hardware = true\" in configuration file, or run with \"--hardware=true\" command line parameter to enable automatic initialisation.")
# Show main application window and run Qt event loop
config.mainwindow.showMaximized()
config.mainwindow.activateWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()