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
+
+
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)