diff --git a/archive_viewer/archive_viewer.py b/archive_viewer/archive_viewer.py index bbabb39..6e69fd8 100644 --- a/archive_viewer/archive_viewer.py +++ b/archive_viewer/archive_viewer.py @@ -1,8 +1,10 @@ -from functools import partial +import os +from logging import (Handler, LogRecord) +from subprocess import run from qtpy.QtCore import Slot -from qtpy.QtWidgets import (QAbstractButton, QApplication) +from qtpy.QtWidgets import (QAbstractButton, QApplication, QLabel) from pydm import Display -from config import logger +from config import (logger, datetime_pv) from mixins import (TracesTableMixin, AxisTableMixin, FileIOMixin) from styles import CenterCheckStyle @@ -11,6 +13,10 @@ class ArchiveViewer(Display, TracesTableMixin, AxisTableMixin, FileIOMixin): def __init__(self, parent=None, args=None, macros=None, ui_filename=__file__.replace(".py", ".ui")) -> None: super(ArchiveViewer, self).__init__(parent=parent, args=args, macros=macros, ui_filename=ui_filename) + self.set_footer() + + app = QApplication.instance() + app.setStyle(CenterCheckStyle()) self.ui.main_spltr.setCollapsible(0, False) self.ui.main_spltr.setStretchFactor(0, 1) @@ -28,7 +34,7 @@ def __init__(self, parent=None, args=None, macros=None, ui_filename=__file__.rep self.ui.week_scale_btn: 604800, self.ui.month_scale_btn: 2628300, self.ui.cursor_scale_btn: -1} - self.ui.timespan_btns.buttonClicked.connect(partial(self.set_plot_timerange)) + self.ui.timespan_btns.buttonClicked.connect(self.set_plot_timerange) plot_viewbox = self.ui.archiver_plot.plotItem.vb plot_viewbox.sigRangeChangedManually.connect(self.ui.cursor_scale_btn.click) @@ -41,6 +47,21 @@ def file_menu_items(self) -> dict: return {"save": (self.export_save_file, "Ctrl+S"), "load": (self.import_save_file, "Ctrl+L")} + def set_footer(self): + """Set footer information for application. Includes logging, nodename, + username, PID, git version, Archiver URL, and current datetime + """ + self.logging_handler = LoggingHandler(self.ui.ftr_logging_lbl) + logger.addHandler(self.logging_handler) + logger.setLevel("NOTSET") + + self.ui.ftr_node_lbl.setText(os.uname().nodename) + self.ui.ftr_user_lbl.setText(os.getlogin()) + self.ui.ftr_pid_lbl.setText(str(os.getpid())) + self.ui.ftr_ver_lbl.setText(self.git_version()) + self.ui.ftr_url_lbl.setText(os.getenv('PYDM_ARCHIVER_URL')) + self.ui.ftr_time_lbl.channel = "ca://" + datetime_pv + @Slot(QAbstractButton) def set_plot_timerange(self, button: QAbstractButton) -> None: """Slot to be called when a timespan setting button is pressed. @@ -54,11 +75,38 @@ def set_plot_timerange(self, button: QAbstractButton) -> None: The timespan setting button pressed. Determines which timespan to set. """ + logger.debug(f"Setting plot timerange") if button not in self.button_spans: logger.error(f"{button} is not a valid timespan button") return enable_scroll = (button != self.ui.cursor_scale_btn) timespan = self.button_spans[button] + if enable_scroll: + logger.debug(f"Enabling plot autoscroll for {timespan}s") + else: + logger.debug("Disabling plot autoscroll, using mouse controls") self.ui.archiver_plot.setAutoScroll(enable_scroll, timespan) + + @staticmethod + def git_version(): + """Get the current git tag for the project""" + project_directory = __file__.rsplit('/', 1)[0] + git_cmd = run(f"cd {project_directory} && git describe --tags", + text=True, + shell=True, + capture_output=True) + return git_cmd.stdout.strip() + + +class LoggingHandler(Handler): + def __init__(self, logging_lbl: QLabel, level: int=0) -> None: + super().__init__(level) + self.logging_lbl = logging_lbl + + def emit(self, record: LogRecord): + log = record.msg + if record.levelno > 20: + log = f"[{record.levelname}] - {log}" + self.logging_lbl.setText(log) diff --git a/archive_viewer/archive_viewer.ui b/archive_viewer/archive_viewer.ui index 8401c94..9720da1 100644 --- a/archive_viewer/archive_viewer.ui +++ b/archive_viewer/archive_viewer.ui @@ -39,6 +39,12 @@ + + + 40 + 16777215 + + 30s @@ -52,6 +58,12 @@ + + + 40 + 16777215 + + 1m @@ -65,6 +77,12 @@ + + + 40 + 16777215 + + 1h @@ -81,6 +99,12 @@ + + + 40 + 16777215 + + 1w @@ -94,6 +118,12 @@ + + + 40 + 16777215 + + 1M @@ -108,7 +138,7 @@ - Cursor + Mouse-Ctrl true @@ -121,19 +151,6 @@ - - - - false - - - Live - - - true - - - @@ -309,9 +326,237 @@ + + + + + + + 0 + 0 + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + true + + + + + + + + + + 8 + + + + Trace Version + + + <version_tag> + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 12 + 75 + true + + + + | + + + + + + + + 8 + + + + nodename + + + <nodename> + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 12 + 75 + true + + + + | + + + + + + + + 8 + + + + user + + + <user> + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 12 + 75 + true + + + + | + + + + + + + + 8 + + + + PID + + + <PID> + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 12 + 75 + true + + + + | + + + + + + + + 8 + + + + Archiver URL + + + <PYDM_ARCHIVER_URL> + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + 0 + + + false + + + true + + + false + + + true + + + Current Datetime + + + + + + false + + + + + + + + + PyDMLabel + QLabel +
pydm.widgets.label
+
PyDMTimePlot QGraphicsView diff --git a/archive_viewer/config.json b/archive_viewer/config.json index 6103731..c078c76 100644 --- a/archive_viewer/config.json +++ b/archive_viewer/config.json @@ -1,5 +1,6 @@ { "save_file_dir": "$PHYSICS_DATA/ArchiveViewer/", + "datetime_pv": "SIOC:SYS0:AL00:TOD", "archivers": { "LCLS": "http://lcls-archapp.slac.stanford.edu" }, diff --git a/archive_viewer/config.py b/archive_viewer/config.py index 5099698..b28f4e9 100644 --- a/archive_viewer/config.py +++ b/archive_viewer/config.py @@ -9,7 +9,9 @@ with config_file.open() as f: loaded_json = load(f) -logger = getLogger(__name__) +logger = getLogger("") + +datetime_pv = loaded_json['datetime_pv'] save_file_dir = Path(os.path.expandvars(loaded_json['save_file_dir'])) if not save_file_dir.is_dir(): diff --git a/archive_viewer/mixins/axis_table.py b/archive_viewer/mixins/axis_table.py index 1abaea6..cfb8cf6 100644 --- a/archive_viewer/mixins/axis_table.py +++ b/archive_viewer/mixins/axis_table.py @@ -3,6 +3,7 @@ from qtpy.QtCore import Slot, QDateTime from qtpy.QtWidgets import QHeaderView from pyqtgraph import ViewBox +from config import logger from table_models import ArchiverAxisModel from widgets import ComboBoxDelegate, ScientificNotationDelegate, DeleteRowDelegate @@ -74,6 +75,7 @@ def set_time_axis_range(self, raw_range: Tuple[QDateTime, QDateTime] = (None, No proc_range[ind] = self.ui.archiver_plot.getXAxis().range[ind] proc_range.sort() + logger.debug(f"Setting plot's X-Axis range to {proc_range}") self.ui.archiver_plot.plotItem.vb.blockSignals(True) self.ui.archiver_plot.plotItem.setXRange(*proc_range) self.ui.archiver_plot.plotItem.vb.blockSignals(False) diff --git a/archive_viewer/mixins/file_io.py b/archive_viewer/mixins/file_io.py index a829fcd..d10a54a 100644 --- a/archive_viewer/mixins/file_io.py +++ b/archive_viewer/mixins/file_io.py @@ -25,10 +25,11 @@ def export_save_file(self) -> None: "Python Archive Viewer (*.pyav)") file_name = Path(file_name) if file_name.is_dir(): - logger.warning("No file name provided") + logger.warning("No file name provided to export save file to") return try: + logger.debug(f"Attempting to export to file: {file_name}") self.io_path = file_name.parent self.converter.export_file(file_name, self.ui.archiver_plot) except FileNotFoundError as e: @@ -45,11 +46,13 @@ def import_save_file(self) -> None: + "All Files (*)") file_name = Path(file_name) if not file_name.is_file(): + logger.warning(f"Attempted import is not a file: {file_name}") return # Import the given file, and convert it from Java Archive Viewer's # format to Trace's format if necessary try: + logger.debug(f"Attempting to import file: {file_name}") file_data = self.converter.import_file(file_name) if self.converter.import_is_xml(): file_data = self.converter.convert_data(file_data) @@ -64,14 +67,16 @@ def import_save_file(self) -> None: import_url = urlparse(file_data['archiver_url']) archiver_url = urlparse(getenv("PYDM_ARCHIVER_URL")) if import_url.hostname != archiver_url.hostname: + logger.warning(f"Attempting to import save file using different Archiver URL: {import_url.hostname}") ret = QMessageBox.warning(self, "Import Error", "The config file you tried to open reads from a different archiver.\n" f"\nCurrent archiver is:\n{archiver_url.hostname}\n" - f"\nAttempted import uses:\n{import_url.hostname}", - QMessageBox.Ok | QMessageBox.Cancel, - QMessageBox.Ok) - if ret == QMessageBox.Cancel: + f"\nAttempted import uses:\n{import_url.hostname}\n\n" + "\nContinue?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No) + if ret == QMessageBox.No: return # Parse the time range for the X-Axis diff --git a/archive_viewer/mixins/traces_table.py b/archive_viewer/mixins/traces_table.py index 555193a..ab34c08 100644 --- a/archive_viewer/mixins/traces_table.py +++ b/archive_viewer/mixins/traces_table.py @@ -110,6 +110,7 @@ def custom_context_menu(self, pos: QPoint) -> None: logger.debug(f"ColorButton column selected: {is_color}") if index.isValid() and not is_color: + logger.debug(f"Opening context menu at index {index}") self.menu.selected_index = index self.menu.popup(table.viewport().mapToGlobal(pos)) diff --git a/archive_viewer/table_models/axis_model.py b/archive_viewer/table_models/axis_model.py index 93c2cc1..fb736bc 100644 --- a/archive_viewer/table_models/axis_model.py +++ b/archive_viewer/table_models/axis_model.py @@ -2,6 +2,7 @@ from qtpy.QtCore import (Qt, QVariant, QPersistentModelIndex, QModelIndex) from pydm.widgets.baseplot import (BasePlot, BasePlotAxisItem) from pydm.widgets.axis_table_model import BasePlotAxesModel +from config import logger class ArchiverAxisModel(BasePlotAxesModel): @@ -63,6 +64,7 @@ def setData(self, index: QModelIndex, value: Any, role=Qt.EditRole) -> bool: The role used by the view to indicate if the model is being editted, by default Qt.EditRole """ + logger.debug(f"Setting {self._column_names[index.column()]} on axis {index.siblingAtColumn(0).data()}") if not index.isValid(): return QVariant() elif role == Qt.CheckStateRole and index.column() in self.checkable_col: @@ -80,6 +82,7 @@ def append(self, name: str = "") -> None: The name for the new axis item. If none is passed in, the axis is named "New Axis ". """ + logger.debug("Adding new empty axis to the plot") if not name: axis_count = self.rowCount() + 1 name = f"New Axis {axis_count}" @@ -120,6 +123,7 @@ def set_model_axes(self, axes: List[Dict]) -> None: clean_a[k] = a[k] cleaned_axes.append(clean_a) + logger.debug("Clearing axes model") self.beginResetModel() self._plot.clearAxes() for a in cleaned_axes: @@ -137,6 +141,7 @@ def removeAtIndex(self, index: QModelIndex) -> None: index : QModelIndex An index in the row to be removed. """ + logger.debug(f"Removing axis at index {index.row()}") if self.rowCount() <= 1: self.append() super().removeAtIndex(index) diff --git a/archive_viewer/table_models/curve_model.py b/archive_viewer/table_models/curve_model.py index 426a51f..afb06fa 100644 --- a/archive_viewer/table_models/curve_model.py +++ b/archive_viewer/table_models/curve_model.py @@ -4,6 +4,7 @@ from pydm.widgets.baseplot import BasePlot from pydm.widgets.archiver_time_plot import ArchivePlotCurveItem from pydm.widgets.archiver_time_plot_editor import PyDMArchiverTimePlotCurvesModel +from config import logger from widgets import ColorButton from table_models import ArchiverAxisModel @@ -60,13 +61,16 @@ def set_data(self, column_name: str, curve: ArchivePlotCurveItem, value: Any) -> bool If the data was successfully set. """ + logger.debug(f"Setting {column_name} data for curve {curve.address}") ret_code = False if column_name == "Channel": if value == curve.address: return True + logger.debug(f"Disconnecting old channel(s): {curve.address}") [ch.disconnect() for ch in curve.channels() if ch] curve.address = str(value) + logger.debug(f"Connecting new channel(s): {curve.address}") [ch.connect() for ch in curve.channels() if ch] if not curve.name(): @@ -82,6 +86,7 @@ def set_data(self, column_name: str, curve: ArchivePlotCurveItem, value: Any) -> else: ret_code = super(ArchiverCurveModel, self).set_data(column_name, curve, value) + logger.debug("Finished setting curve data") return ret_code def append(self, address: Optional[str] = None, name: Optional[str] = None, color: Optional[QColor] = None) -> None: @@ -93,9 +98,10 @@ def append(self, address: Optional[str] = None, name: Optional[str] = None, colo The PV address that the curve should gather data from. name : str, optional The display name for the curve. - color : Optional[QColor], optional + color : QColor, optional The curve's color on the plot. """ + logger.debug("Adding new empty curve to plot") if self.rowCount() != 1: self._axis_model.append() y_axis = self._axis_model.get_axis(-1) @@ -106,13 +112,23 @@ def append(self, address: Optional[str] = None, name: Optional[str] = None, colo self.beginInsertRows(QModelIndex(), len(self._plot._curves), len(self._plot._curves)) self._plot.addYChannel(y_channel=address, name=name, color=color, useArchiveData=True, yAxisName=y_axis.name) self.endInsertRows() + logger.debug("Finished adding new empty curve to plot") def set_model_curves(self, curves: List[Dict]) -> None: + """Reset model curves to given list of curve properties. + + Parameters + ---------- + curves : List[Dict] + List of curve properties. + """ + logger.debug("Clearing curves model.") self.beginResetModel() self._plot.clearCurves() self._row_names = [] for c in curves: + logger.debug(f"Adding curve: {c['channel']}") for k, v in c.items(): if v is None: del c[k] @@ -121,8 +137,8 @@ def set_model_curves(self, curves: List[Dict]) -> None: self._plot.addYChannel(**c) self._row_names.append(self.next_header()) self.append() - self.endResetModel() + logger.debug("Finished setting curves model") def removeAtIndex(self, index: QModelIndex) -> None: """Removes the curve at the given table index. @@ -132,6 +148,7 @@ def removeAtIndex(self, index: QModelIndex) -> None: index : QModelIndex An index in the row to be removed. """ + logger.debug(f"Removing curve at index {index.row()}") if not index.isValid() or index.row() == (self.rowCount() - 1): return False del self._row_names[index.row()] @@ -139,9 +156,11 @@ def removeAtIndex(self, index: QModelIndex) -> None: if not self._plot._curves: self.append() + logger.debug(f"Finished removing curve previously at index {index.row()}") return ret def headerData(self, section, orientation, role=Qt.DisplayRole) -> Any: + """Return row header for given index""" if role == Qt.DisplayRole and orientation == Qt.Vertical and section < self.rowCount(): return self._row_names[section] return super().headerData(section, orientation, role)